• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / brighterscript / #12849

25 Jul 2024 12:51PM UTC coverage: 86.205%. Remained the same
#12849

push

web-flow
Merge 0c17734d3 into 5f3ffa3fa

10601 of 13088 branches covered (81.0%)

Branch coverage included in aggregate %.

82 of 88 new or added lines in 15 files covered. (93.18%)

288 existing lines in 16 files now uncovered.

12302 of 13480 relevant lines covered (91.26%)

26603.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.24
/src/bscPlugin/validation/ScopeValidator.ts
1
import { URI } from 'vscode-uri';
1✔
2
import { DiagnosticTag, type Range } from 'vscode-languageserver';
1✔
3
import { isAliasStatement, isAssignmentStatement, isAssociativeArrayType, isBinaryExpression, isBooleanType, isBrsFile, isCallExpression, isCallableType, isClassStatement, isClassType, isComponentType, isDottedGetExpression, isDynamicType, isEnumMemberType, isEnumType, isFunctionExpression, isFunctionParameterExpression, isLiteralExpression, isNamespaceStatement, isNamespaceType, isNewExpression, isNumberType, isObjectType, isPrimitiveType, isReferenceType, isStringType, isTypedFunctionType, isUnionType, isVariableExpression, isXmlScope } from '../../astUtils/reflection';
1✔
4
import type { DiagnosticInfo } from '../../DiagnosticMessages';
5
import { DiagnosticMessages } from '../../DiagnosticMessages';
1✔
6
import type { BrsFile } from '../../files/BrsFile';
7
import type { BsDiagnostic, CallableContainer, ExtraSymbolData, FileReference, GetTypeOptions, OnScopeValidateEvent, TypeChainEntry, TypeChainProcessResult, TypeCompatibilityData } from '../../interfaces';
8
import { SymbolTypeFlag } from '../../SymbolTypeFlag';
1✔
9
import type { AssignmentStatement, AugmentedAssignmentStatement, ClassStatement, DottedSetStatement, IncrementStatement, NamespaceStatement, ReturnStatement } from '../../parser/Statement';
10
import { util } from '../../util';
1✔
11
import { nodes, components } from '../../roku-types';
1✔
12
import type { BRSComponentData } from '../../roku-types';
13
import type { Token } from '../../lexer/Token';
14
import { AstNodeKind } from '../../parser/AstNode';
1✔
15
import type { AstNode } from '../../parser/AstNode';
16
import type { Expression } from '../../parser/AstNode';
17
import type { VariableExpression, DottedGetExpression, BinaryExpression, UnaryExpression, NewExpression, LiteralExpression } from '../../parser/Expression';
18
import { CallExpression } from '../../parser/Expression';
1✔
19
import { createVisitor } from '../../astUtils/visitors';
1✔
20
import type { BscType } from '../../types/BscType';
21
import type { BscFile } from '../../files/BscFile';
22
import { InsideSegmentWalkMode } from '../../AstValidationSegmenter';
1✔
23
import { TokenKind } from '../../lexer/TokenKind';
1✔
24
import { ParseMode } from '../../parser/Parser';
1✔
25
import { BsClassValidator } from '../../validators/ClassValidator';
1✔
26
import { globalCallableMap } from '../../globalCallables';
1✔
27
import type { XmlScope } from '../../XmlScope';
28
import type { XmlFile } from '../../files/XmlFile';
29
import { SGFieldTypes } from '../../parser/SGTypes';
1✔
30
import { DynamicType } from '../../types';
1✔
31
import { BscTypeKind } from '../../types/BscTypeKind';
1✔
32

33
/**
34
 * The lower-case names of all platform-included scenegraph nodes
35
 */
36
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
37
const platformNodeNames = nodes ? new Set((Object.values(nodes) as { name: string }[]).map(x => x?.name.toLowerCase())) : new Set();
95!
38
const platformComponentNames = components ? new Set((Object.values(components) as { name: string }[]).map(x => x?.name.toLowerCase())) : new Set();
64!
39

40
const enum ScopeValidatorDiagnosticTag {
1✔
41
    Imports = 'ScopeValidatorImports',
1✔
42
    NamespaceCollisions = 'ScopeValidatorNamespaceCollisions',
1✔
43
    DuplicateFunctionDeclaration = 'ScopeValidatorDuplicateFunctionDeclaration',
1✔
44
    FunctionCollisions = 'ScopeValidatorFunctionCollisions',
1✔
45
    Classes = 'ScopeValidatorClasses',
1✔
46
    XMLInterface = 'ScopeValidatorXML',
1✔
47
    XMLImports = 'ScopeValidatorXMLImports',
1✔
48
    Default = 'ScopeValidator',
1✔
49
    Segment = 'ScopeValidatorSegment'
1✔
50
}
51

52
/**
53
 * A validator that handles all scope validations for a program validation cycle.
54
 * You should create ONE of these to handle all scope events between beforeProgramValidate and afterProgramValidate,
55
 * and call reset() before using it again in the next cycle
56
 */
57
export class ScopeValidator {
1✔
58

59
    /**
60
     * The event currently being processed. This will change multiple times throughout the lifetime of this validator
61
     */
62
    private event: OnScopeValidateEvent;
63

64
    private metrics = new Map<string, number>();
1,535✔
65

66

67
    public processEvent(event: OnScopeValidateEvent) {
68
        this.event = event;
3,073✔
69
        if (this.event.program.globalScope === this.event.scope) {
3,073✔
70
            return;
1,535✔
71
        }
72
        this.metrics.clear();
1,538✔
73
        this.walkFiles();
1,538✔
74
        this.currentSegmentBeingValidated = null;
1,538✔
75
        this.flagDuplicateFunctionDeclarations();
1,538✔
76
        this.validateScriptImportPaths();
1,538✔
77
        this.validateClasses();
1,538✔
78
        if (isXmlScope(event.scope)) {
1,538✔
79
            //detect when the child imports a script that its ancestor also imports
80
            this.diagnosticDetectDuplicateAncestorScriptImports(event.scope);
428✔
81
            //validate component interface
82
            this.validateXmlInterface(event.scope);
428✔
83
        }
84

85
        this.event.program.logger.debug(this.event.scope.name, 'metrics:');
1,538✔
86
        let total = 0;
1,538✔
87
        for (const [filePath, num] of this.metrics) {
1,538✔
88
            this.event.program.logger.debug(' - ', filePath, num);
1,500✔
89
            total += num;
1,500✔
90
        }
91
        this.event.program.logger.debug(this.event.scope.name, 'total segments validated', total);
1,538✔
92
    }
93

94
    public reset() {
95
        this.event = undefined;
1,226✔
96
    }
97

98
    private walkFiles() {
99
        const hasChangeInfo = this.event.changedFiles && this.event.changedSymbols;
1,538✔
100

101
        //do many per-file checks for every file in this (and parent) scopes
102
        this.event.scope.enumerateBrsFiles((file) => {
1,538✔
103
            if (!isBrsFile(file)) {
1,908!
104
                return;
×
105
            }
106

107
            const thisFileHasChanges = this.event.changedFiles.includes(file);
1,908✔
108

109
            this.detectVariableNamespaceCollisions(file);
1,908✔
110

111
            if (thisFileHasChanges || this.doesFileProvideChangedSymbol(file, this.event.changedSymbols)) {
1,908✔
112
                this.diagnosticDetectFunctionCollisions(file);
1,765✔
113
            }
114
        });
115

116
        this.event.scope.enumerateOwnFiles((file) => {
1,538✔
117
            if (isBrsFile(file)) {
2,326✔
118

119
                const fileUri = util.pathToUri(file.srcPath);
1,898✔
120
                const thisFileHasChanges = this.event.changedFiles.includes(file);
1,898✔
121

122
                const hasUnvalidatedSegments = file.validationSegmenter.hasUnvalidatedSegments();
1,898✔
123

124
                if (hasChangeInfo && !hasUnvalidatedSegments) {
1,898✔
125
                    return;
398✔
126
                }
127

128
                const validationVisitor = createVisitor({
1,500✔
129
                    VariableExpression: (varExpr) => {
130
                        this.validateVariableAndDottedGetExpressions(file, varExpr);
3,304✔
131
                    },
132
                    DottedGetExpression: (dottedGet) => {
133
                        this.validateVariableAndDottedGetExpressions(file, dottedGet);
1,376✔
134
                    },
135
                    CallExpression: (functionCall) => {
136
                        this.validateFunctionCall(file, functionCall);
833✔
137
                        this.validateCreateObjectCall(file, functionCall);
833✔
138
                    },
139
                    ReturnStatement: (returnStatement) => {
140
                        this.validateReturnStatement(file, returnStatement);
244✔
141
                    },
142
                    DottedSetStatement: (dottedSetStmt) => {
143
                        this.validateDottedSetStatement(file, dottedSetStmt);
73✔
144
                    },
145
                    BinaryExpression: (binaryExpr) => {
146
                        this.validateBinaryExpression(file, binaryExpr);
223✔
147
                    },
148
                    UnaryExpression: (unaryExpr) => {
149
                        this.validateUnaryExpression(file, unaryExpr);
33✔
150
                    },
151
                    AssignmentStatement: (assignStmt) => {
152
                        this.validateAssignmentStatement(file, assignStmt);
576✔
153
                        // Note: this also includes For statements
154
                        this.detectShadowedLocalVar(file, {
576✔
155
                            name: assignStmt.tokens.name.text,
156
                            type: this.getNodeTypeWrapper(file, assignStmt, { flags: SymbolTypeFlag.runtime }),
157
                            nameRange: assignStmt.tokens.name.location?.range
1,728✔
158
                        });
159
                    },
160
                    AugmentedAssignmentStatement: (binaryExpr) => {
161
                        this.validateBinaryExpression(file, binaryExpr);
48✔
162
                    },
163
                    IncrementStatement: (stmt) => {
164
                        this.validateIncrementStatement(file, stmt);
9✔
165
                    },
166
                    NewExpression: (newExpr) => {
167
                        this.validateNewExpression(file, newExpr);
113✔
168
                    },
169
                    ForEachStatement: (forEachStmt) => {
170
                        this.detectShadowedLocalVar(file, {
22✔
171
                            name: forEachStmt.tokens.item.text,
172
                            type: this.getNodeTypeWrapper(file, forEachStmt, { flags: SymbolTypeFlag.runtime }),
173
                            nameRange: forEachStmt.tokens.item.location?.range
66✔
174
                        });
175
                    },
176
                    FunctionParameterExpression: (funcParam) => {
177
                        this.detectShadowedLocalVar(file, {
1,018✔
178
                            name: funcParam.tokens.name.text,
179
                            type: this.getNodeTypeWrapper(file, funcParam, { flags: SymbolTypeFlag.runtime }),
180
                            nameRange: funcParam.tokens.name.location?.range
3,054✔
181
                        });
182
                    }
183
                });
184
                // validate only what's needed in the file
185

186
                const segmentsToWalkForValidation = thisFileHasChanges
1,500✔
187
                    ? file.validationSegmenter.getAllUnvalidatedSegments()
1,500✔
188
                    : file.validationSegmenter.getSegmentsWithChangedSymbols(this.event.changedSymbols);
189

190
                let segmentsValidated = 0;
1,500✔
191
                for (const segment of segmentsToWalkForValidation) {
1,500✔
192
                    if (!file.validationSegmenter.checkIfSegmentNeedsRevalidation(segment, this.event.changedSymbols)) {
2,911!
UNCOV
193
                        continue;
×
194
                    }
195
                    this.currentSegmentBeingValidated = segment;
2,911✔
196
                    this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, segment: segment, tag: ScopeValidatorDiagnosticTag.Segment });
2,911✔
197
                    segmentsValidated++;
2,911✔
198
                    segment.walk(validationVisitor, {
2,911✔
199
                        walkMode: InsideSegmentWalkMode
200
                    });
201
                    file.markSegmentAsValidated(segment);
2,911✔
202
                    this.currentSegmentBeingValidated = null;
2,911✔
203
                }
204
                this.metrics.set(file.pkgPath, segmentsValidated);
1,500✔
205
            }
206
        });
207
    }
208

209
    private doesFileProvideChangedSymbol(file: BrsFile, changedSymbols: Map<SymbolTypeFlag, Set<string>>) {
210
        if (!changedSymbols) {
143!
UNCOV
211
            return true;
×
212
        }
213
        for (const flag of [SymbolTypeFlag.runtime, SymbolTypeFlag.typetime]) {
143✔
214
            const providedSymbolKeysFlag = file.providedSymbols.symbolMap.get(flag).keys();
286✔
215
            const changedSymbolSetForFlag = changedSymbols.get(flag);
286✔
216

217
            for (let providedKey of providedSymbolKeysFlag) {
286✔
218
                if (changedSymbolSetForFlag.has(providedKey)) {
268!
UNCOV
219
                    return true;
×
220
                }
221
            }
222
        }
223
        return false;
143✔
224
    }
225

226
    private currentSegmentBeingValidated: AstNode;
227

228

229
    private isTypeKnown(exprType: BscType) {
230
        let isKnownType = exprType?.isResolvable();
3,403✔
231
        return isKnownType;
3,403✔
232
    }
233

234
    /**
235
     * If this is the lhs of an assignment, we don't need to flag it as unresolved
236
     */
237
    private hasValidDeclaration(expression: Expression, exprType: BscType, definingNode?: AstNode) {
238
        if (!isVariableExpression(expression)) {
3,403✔
239
            return false;
975✔
240
        }
241
        let assignmentAncestor: AssignmentStatement;
242
        if (isAssignmentStatement(definingNode) && definingNode.tokens.equals.kind === TokenKind.Equal) {
2,428✔
243
            // this symbol was defined in a "normal" assignment (eg. not a compound assignment)
244
            assignmentAncestor = definingNode;
285✔
245
            return assignmentAncestor?.tokens.name?.text.toLowerCase() === expression?.tokens.name?.text.toLowerCase();
285!
246
        } else if (isFunctionParameterExpression(definingNode)) {
2,143✔
247
            // this symbol was defined in a function param
248
            return true;
428✔
249
        } else {
250
            assignmentAncestor = expression?.findAncestor(isAssignmentStatement);
1,715!
251
        }
252
        return assignmentAncestor?.tokens.name === expression?.tokens.name && isUnionType(exprType);
1,715!
253
    }
254

255
    /**
256
     * Validate every function call to `CreateObject`.
257
     * Ideally we would create better type checking/handling for this, but in the mean time, we know exactly
258
     * what these calls are supposed to look like, and this is a very common thing for brs devs to do, so just
259
     * do this manually for now.
260
     */
261
    protected validateCreateObjectCall(file: BrsFile, call: CallExpression) {
262

263
        //skip non CreateObject function calls
264
        const callName = util.getAllDottedGetPartsAsString(call.callee)?.toLowerCase();
833✔
265
        if (callName !== 'createobject' || !isLiteralExpression(call?.args[0])) {
833!
266
            return;
772✔
267
        }
268
        const firstParamToken = (call?.args[0] as LiteralExpression)?.tokens?.value;
61!
269
        const firstParamStringValue = firstParamToken?.text?.replace(/"/g, '');
61!
270
        if (!firstParamStringValue) {
61!
UNCOV
271
            return;
×
272
        }
273
        const firstParamStringValueLower = firstParamStringValue.toLowerCase();
61✔
274

275
        //if this is a `createObject('roSGNode'` call, only support known sg node types
276
        if (firstParamStringValueLower === 'rosgnode' && isLiteralExpression(call?.args[1])) {
61!
277
            const componentName: Token = call?.args[1]?.tokens.value;
25!
278
            //don't validate any components with a colon in their name (probably component libraries, but regular components can have them too).
279
            if (!componentName || componentName?.text?.includes(':')) {
25!
280
                return;
3✔
281
            }
282
            //add diagnostic for unknown components
283
            const unquotedComponentName = componentName?.text?.replace(/"/g, '');
22!
284
            if (unquotedComponentName && !platformNodeNames.has(unquotedComponentName.toLowerCase()) && !this.event.program.getComponent(unquotedComponentName)) {
22✔
285
                this.addDiagnostic({
4✔
286
                    ...DiagnosticMessages.unknownRoSGNode(unquotedComponentName),
287
                    location: componentName.location
288
                });
289
            } else if (call?.args.length !== 2) {
18!
290
                // roSgNode should only ever have 2 args in `createObject`
291
                this.addDiagnostic({
1✔
292
                    ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, [2], call?.args.length),
3!
293
                    location: call.location
294
                });
295
            }
296
        } else if (!platformComponentNames.has(firstParamStringValueLower)) {
36✔
297
            this.addDiagnostic({
7✔
298
                ...DiagnosticMessages.unknownBrightScriptComponent(firstParamStringValue),
299
                location: firstParamToken.location
300
            });
301
        } else {
302
            // This is valid brightscript component
303
            // Test for invalid arg counts
304
            const brightScriptComponent: BRSComponentData = components[firstParamStringValueLower];
29✔
305
            // Valid arg counts for createObject are 1+ number of args for constructor
306
            let validArgCounts = brightScriptComponent?.constructors.map(cnstr => cnstr.params.length + 1);
33!
307
            if (validArgCounts.length === 0) {
29✔
308
                // no constructors for this component, so createObject only takes 1 arg
309
                validArgCounts = [1];
1✔
310
            }
311
            if (!validArgCounts.includes(call?.args.length)) {
29!
312
                // Incorrect number of arguments included in `createObject()`
313
                this.addDiagnostic({
4✔
314
                    ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, validArgCounts, call?.args.length),
12!
315
                    location: call.location
316
                });
317
            }
318

319
            // Test for deprecation
320
            if (brightScriptComponent?.isDeprecated) {
29!
UNCOV
321
                this.addDiagnostic({
×
322
                    ...DiagnosticMessages.deprecatedBrightScriptComponent(firstParamStringValue, brightScriptComponent.deprecatedDescription),
323
                    location: call.location
324
                });
325
            }
326
        }
327

328
    }
329

330
    /**
331
     * Detect calls to functions with the incorrect number of parameters, or wrong types of arguments
332
     */
333
    private validateFunctionCall(file: BrsFile, expression: CallExpression) {
334
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: {} };
833✔
335
        let funcType = this.getNodeTypeWrapper(file, expression?.callee, getTypeOptions);
833!
336
        if (funcType?.isResolvable() && isClassType(funcType)) {
833✔
337
            // We're calling a class - get the constructor
338
            funcType = funcType.getMemberType('new', getTypeOptions);
121✔
339
        }
340
        if (funcType?.isResolvable() && isTypedFunctionType(funcType)) {
833✔
341
            //funcType.setName(expression.callee. .name);
342

343
            //get min/max parameter count for callable
344
            let minParams = 0;
588✔
345
            let maxParams = 0;
588✔
346
            for (let param of funcType.params) {
588✔
347
                maxParams++;
828✔
348
                //optional parameters must come last, so we can assume that minParams won't increase once we hit
349
                //the first isOptional
350
                if (param.isOptional !== true) {
828✔
351
                    minParams++;
451✔
352
                }
353
            }
354
            if (funcType.isVariadic) {
588✔
355
                // function accepts variable number of arguments
356
                maxParams = CallExpression.MaximumArguments;
3✔
357
            }
358
            let expCallArgCount = expression.args.length;
588✔
359
            if (expCallArgCount > maxParams || expCallArgCount < minParams) {
588✔
360
                let minMaxParamsText = minParams === maxParams ? maxParams : `${minParams}-${maxParams}`;
31✔
361
                this.addMultiScopeDiagnostic({
31✔
362
                    ...DiagnosticMessages.mismatchArgumentCount(minMaxParamsText, expCallArgCount),
363
                    location: expression.callee.location
364
                });
365
            }
366
            let paramIndex = 0;
588✔
367
            for (let arg of expression.args) {
588✔
368
                const data = {} as ExtraSymbolData;
521✔
369
                let argType = this.getNodeTypeWrapper(file, arg, { flags: SymbolTypeFlag.runtime, data: data });
521✔
370

371
                const paramType = funcType.params[paramIndex]?.type;
521✔
372
                if (!paramType) {
521✔
373
                    // unable to find a paramType -- maybe there are more args than params
374
                    break;
16✔
375
                }
376

377
                if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) {
505✔
378
                    // the param is expecting a function, but we're passing a Class... are we actually passing the constructor? then we're ok!
379
                    const namespace = expression.findAncestor<NamespaceStatement>(isNamespaceStatement);
2✔
380
                    if (file.calleeIsKnownFunction(arg, namespace?.getName(ParseMode.BrighterScript))) {
2!
381
                        argType = data.definingNode.getConstructorType();
2✔
382
                    }
383
                }
384

385
                const compatibilityData: TypeCompatibilityData = {};
505✔
386
                const isAllowedArgConversion = this.checkAllowedArgConversions(paramType, argType);
505✔
387
                if (!isAllowedArgConversion && !paramType?.isTypeCompatible(argType, compatibilityData)) {
505!
388
                    this.addMultiScopeDiagnostic({
32✔
389
                        ...DiagnosticMessages.argumentTypeMismatch(argType.toString(), paramType.toString(), compatibilityData),
390
                        location: arg.location
391
                    });
392
                }
393
                paramIndex++;
505✔
394
            }
395

396
        }
397
    }
398

399
    private checkAllowedArgConversions(paramType: BscType, argType: BscType): boolean {
400
        if (isNumberType(argType) && isBooleanType(paramType)) {
505✔
401
            return true;
8✔
402
        }
403
        return false;
497✔
404
    }
405

406

407
    /**
408
     * Detect return statements with incompatible types vs. declared return type
409
     */
410
    private validateReturnStatement(file: BrsFile, returnStmt: ReturnStatement) {
411
        const getTypeOptions = { flags: SymbolTypeFlag.runtime };
244✔
412
        let funcType = returnStmt.findAncestor(isFunctionExpression).getType({ flags: SymbolTypeFlag.typetime });
244✔
413
        if (isTypedFunctionType(funcType)) {
244!
414
            const actualReturnType = this.getNodeTypeWrapper(file, returnStmt?.value, getTypeOptions);
244!
415
            const compatibilityData: TypeCompatibilityData = {};
244✔
416

417
            if (actualReturnType && !funcType.returnType.isTypeCompatible(actualReturnType, compatibilityData)) {
244✔
418
                this.addMultiScopeDiagnostic({
12✔
419
                    ...DiagnosticMessages.returnTypeMismatch(actualReturnType.toString(), funcType.returnType.toString(), compatibilityData),
420
                    location: returnStmt.value.location
421
                });
422

423
            }
424
        }
425
    }
426

427
    /**
428
     * Detect assigned type different from expected member type
429
     */
430
    private validateDottedSetStatement(file: BrsFile, dottedSetStmt: DottedSetStatement) {
431
        const typeChainExpectedLHS = [] as TypeChainEntry[];
73✔
432
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
73✔
433

434
        const expectedLHSType = this.getNodeTypeWrapper(file, dottedSetStmt, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
73✔
435
        const actualRHSType = this.getNodeTypeWrapper(file, dottedSetStmt?.value, getTypeOpts);
73!
436
        const compatibilityData: TypeCompatibilityData = {};
73✔
437
        const typeChainScan = util.processTypeChain(typeChainExpectedLHS);
73✔
438
        // check if anything in typeChain is an AA - if so, just allow it
439
        if (typeChainExpectedLHS.find(typeChainItem => isAssociativeArrayType(typeChainItem.type))) {
124✔
440
            // something in the chain is an AA
441
            // treat members as dynamic - they could have been set without the type system's knowledge
442
            return;
28✔
443
        }
444
        if (!expectedLHSType || !expectedLHSType?.isResolvable()) {
45!
445
            this.addMultiScopeDiagnostic({
7✔
446
                ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
447
                location: typeChainScan?.location
21!
448
            });
449
            return;
7✔
450
        }
451

452
        let accessibilityIsOk = this.checkMemberAccessibility(file, dottedSetStmt, typeChainExpectedLHS);
38✔
453

454
        //Most Component fields can be set with strings
455
        //TODO: be more precise about which fields can actually accept strings
456
        //TODO: if RHS is a string literal, we can do more validation to make sure it's the correct type
457
        if (isComponentType(dottedSetStmt.obj?.getType({ flags: SymbolTypeFlag.runtime }))) {
38!
458
            if (isStringType(actualRHSType)) {
12✔
459
                return;
4✔
460
            }
461
        }
462

463
        if (accessibilityIsOk && !expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
34!
464
            this.addMultiScopeDiagnostic({
7✔
465
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
466
                location: dottedSetStmt.location
467
            });
468
        }
469
    }
470

471
    /**
472
     * Detect when declared type does not match rhs type
473
     */
474
    private validateAssignmentStatement(file: BrsFile, assignStmt: AssignmentStatement) {
475
        if (!assignStmt?.typeExpression) {
576!
476
            // nothing to check
477
            return;
570✔
478
        }
479

480
        const typeChainExpectedLHS = [];
6✔
481
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
6✔
482
        const expectedLHSType = this.getNodeTypeWrapper(file, assignStmt.typeExpression, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
6✔
483
        const actualRHSType = this.getNodeTypeWrapper(file, assignStmt.value, getTypeOpts);
6✔
484
        const compatibilityData: TypeCompatibilityData = {};
6✔
485
        if (!expectedLHSType || !expectedLHSType.isResolvable()) {
6✔
486
            // LHS is not resolvable... handled elsewhere
487
        } else if (!expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
5!
488
            this.addMultiScopeDiagnostic({
1✔
489
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
490
                location: assignStmt.location
491
            });
492
        }
493
    }
494

495
    /**
496
     * Detect invalid use of a binary operator
497
     */
498
    private validateBinaryExpression(file: BrsFile, binaryExpr: BinaryExpression | AugmentedAssignmentStatement) {
499
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
271✔
500

501
        if (util.isInTypeExpression(binaryExpr)) {
271✔
502
            return;
13✔
503
        }
504

505
        let leftType = isBinaryExpression(binaryExpr)
258✔
506
            ? this.getNodeTypeWrapper(file, binaryExpr.left, getTypeOpts)
258✔
507
            : this.getNodeTypeWrapper(file, binaryExpr.item, getTypeOpts);
508
        let rightType = isBinaryExpression(binaryExpr)
258✔
509
            ? this.getNodeTypeWrapper(file, binaryExpr.right, getTypeOpts)
258✔
510
            : this.getNodeTypeWrapper(file, binaryExpr.value, getTypeOpts);
511

512
        if (!leftType.isResolvable() || !rightType.isResolvable()) {
258✔
513
            // Can not find the type. error handled elsewhere
514
            return;
10✔
515
        }
516
        let leftTypeToTest = leftType;
248✔
517
        let rightTypeToTest = rightType;
248✔
518

519
        if (isEnumMemberType(leftType) || isEnumType(leftType)) {
248✔
520
            leftTypeToTest = leftType.underlyingType;
11✔
521
        }
522
        if (isEnumMemberType(rightType) || isEnumType(rightType)) {
248✔
523
            rightTypeToTest = rightType.underlyingType;
10✔
524
        }
525

526
        if (isUnionType(leftType) || isUnionType(rightType)) {
248!
527
            // TODO: it is possible to validate based on innerTypes, but more complicated
528
            // Because you need to verify each combination of types
UNCOV
529
            return;
×
530
        }
531
        const leftIsPrimitive = isPrimitiveType(leftTypeToTest);
248✔
532
        const rightIsPrimitive = isPrimitiveType(rightTypeToTest);
248✔
533
        const leftIsAny = isDynamicType(leftTypeToTest) || isObjectType(leftTypeToTest);
248✔
534
        const rightIsAny = isDynamicType(rightTypeToTest) || isObjectType(rightTypeToTest);
248✔
535

536

537
        if (leftIsAny && rightIsAny) {
248✔
538
            // both operands are basically "any" type... ignore;
539
            return;
19✔
540
        } else if ((leftIsAny && rightIsPrimitive) || (leftIsPrimitive && rightIsAny)) {
229✔
541
            // one operand is basically "any" type... ignore;
542
            return;
42✔
543
        }
544
        const opResult = util.binaryOperatorResultType(leftTypeToTest, binaryExpr.tokens.operator, rightTypeToTest);
187✔
545

546
        if (isDynamicType(opResult)) {
187✔
547
            // if the result was dynamic, that means there wasn't a valid operation
548
            this.addMultiScopeDiagnostic({
7✔
549
                ...DiagnosticMessages.operatorTypeMismatch(binaryExpr.tokens.operator.text, leftType.toString(), rightType.toString()),
550
                location: binaryExpr.location
551
            });
552
        }
553
    }
554

555
    /**
556
     * Detect invalid use of a Unary operator
557
     */
558
    private validateUnaryExpression(file: BrsFile, unaryExpr: UnaryExpression) {
559
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
33✔
560

561
        let rightType = this.getNodeTypeWrapper(file, unaryExpr.right, getTypeOpts);
33✔
562

563
        if (!rightType.isResolvable()) {
33!
564
            // Can not find the type. error handled elsewhere
UNCOV
565
            return;
×
566
        }
567
        let rightTypeToTest = rightType;
33✔
568
        if (isEnumMemberType(rightType)) {
33!
UNCOV
569
            rightTypeToTest = rightType.underlyingType;
×
570
        }
571

572

573
        if (isUnionType(rightTypeToTest)) {
33✔
574
            // TODO: it is possible to validate based on innerTypes, but more complicated
575
            // Because you need to verify each combination of types
576

577
        } else if (isDynamicType(rightTypeToTest) || isObjectType(rightTypeToTest)) {
32✔
578
            // operand is basically "any" type... ignore;
579

580
        } else if (isPrimitiveType(rightType)) {
27!
581
            const opResult = util.unaryOperatorResultType(unaryExpr.tokens.operator, rightTypeToTest);
27✔
582
            if (isDynamicType(opResult)) {
27✔
583
                this.addMultiScopeDiagnostic({
1✔
584
                    ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
585
                    location: unaryExpr.location
586
                });
587
            }
588
        } else {
589
            // rhs is not a primitive, so no binary operator is allowed
UNCOV
590
            this.addMultiScopeDiagnostic({
×
591
                ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
592
                location: unaryExpr.location
593
            });
594
        }
595
    }
596

597
    private validateIncrementStatement(file: BrsFile, incStmt: IncrementStatement) {
598
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
9✔
599

600
        let rightType = this.getNodeTypeWrapper(file, incStmt.value, getTypeOpts);
9✔
601

602
        if (!rightType.isResolvable()) {
9!
603
            // Can not find the type. error handled elsewhere
UNCOV
604
            return;
×
605
        }
606

607
        if (isUnionType(rightType)) {
9!
608
            // TODO: it is possible to validate based on innerTypes, but more complicated
609
            // because you need to verify each combination of types
610
        } else if (isDynamicType(rightType) || isObjectType(rightType)) {
9✔
611
            // operand is basically "any" type... ignore
612
        } else if (isNumberType(rightType)) {
7✔
613
            // operand is a number.. this is ok
614
        } else {
615
            // rhs is not a number, so no increment operator is not allowed
616
            this.addMultiScopeDiagnostic({
1✔
617
                ...DiagnosticMessages.operatorTypeMismatch(incStmt.tokens.operator.text, rightType.toString()),
618
                location: incStmt.location
619
            });
620
        }
621
    }
622

623

624
    validateVariableAndDottedGetExpressions(file: BrsFile, expression: VariableExpression | DottedGetExpression) {
625
        if (isDottedGetExpression(expression.parent)) {
4,680✔
626
            // We validate dottedGetExpressions at the top-most level
627
            return;
1,274✔
628
        }
629
        if (isVariableExpression(expression)) {
3,406✔
630
            if (isAssignmentStatement(expression.parent) && expression.parent.tokens.name === expression.tokens.name) {
2,431!
631
                // Don't validate LHS of assignments
UNCOV
632
                return;
×
633
            } else if (isNamespaceStatement(expression.parent)) {
2,431✔
634
                return;
3✔
635
            }
636
        }
637

638
        let symbolType = SymbolTypeFlag.runtime;
3,403✔
639
        let oppositeSymbolType = SymbolTypeFlag.typetime;
3,403✔
640
        const isUsedAsType = util.isInTypeExpression(expression);
3,403✔
641
        if (isUsedAsType) {
3,403✔
642
            // This is used in a TypeExpression - only look up types from SymbolTable
643
            symbolType = SymbolTypeFlag.typetime;
1,087✔
644
            oppositeSymbolType = SymbolTypeFlag.runtime;
1,087✔
645
        }
646

647
        // Do a complete type check on all DottedGet and Variable expressions
648
        // this will create a diagnostic if an invalid member is accessed
649
        const typeChain: TypeChainEntry[] = [];
3,403✔
650
        const typeData = {} as ExtraSymbolData;
3,403✔
651
        let exprType = this.getNodeTypeWrapper(file, expression, {
3,403✔
652
            flags: symbolType,
653
            typeChain: typeChain,
654
            data: typeData
655
        });
656

657
        const hasValidDeclaration = this.hasValidDeclaration(expression, exprType, typeData?.definingNode);
3,403!
658

659
        //include a hint diagnostic if this type is marked as deprecated
660
        if (typeData.flags & SymbolTypeFlag.deprecated) { // eslint-disable-line no-bitwise
3,403✔
661
            this.addMultiScopeDiagnostic({
2✔
662
                ...DiagnosticMessages.itemIsDeprecated(),
663
                location: expression.tokens.name.location,
664
                tags: [DiagnosticTag.Deprecated]
665
            });
666
        }
667

668
        if (!this.isTypeKnown(exprType) && !hasValidDeclaration) {
3,403✔
669
            if (this.getNodeTypeWrapper(file, expression, { flags: oppositeSymbolType, isExistenceTest: true })?.isResolvable()) {
227!
670
                const oppoSiteTypeChain = [];
5✔
671
                const invalidlyUsedResolvedType = this.getNodeTypeWrapper(file, expression, { flags: oppositeSymbolType, typeChain: oppoSiteTypeChain, isExistenceTest: true });
5✔
672
                const typeChainScan = util.processTypeChain(oppoSiteTypeChain);
5✔
673
                if (isUsedAsType) {
5✔
674
                    this.addMultiScopeDiagnostic({
2✔
675
                        ...DiagnosticMessages.itemCannotBeUsedAsType(typeChainScan.fullChainName),
676
                        location: expression.location
677
                    });
678
                } else if (invalidlyUsedResolvedType && !isReferenceType(invalidlyUsedResolvedType)) {
3✔
679
                    if (!isAliasStatement(expression.parent)) {
1!
680
                        // alias rhs CAN be a type!
UNCOV
681
                        this.addMultiScopeDiagnostic({
×
682
                            ...DiagnosticMessages.itemCannotBeUsedAsVariable(invalidlyUsedResolvedType.toString()),
683
                            location: expression.location
684
                        });
685
                    }
686
                } else {
687
                    const typeChainScan = util.processTypeChain(typeChain);
2✔
688
                    //if this is a function call, provide a different diganostic code
689
                    if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
2✔
690
                        this.addMultiScopeDiagnostic({
1✔
691
                            ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
692
                            location: typeChainScan?.location
3!
693
                        });
694
                    } else {
695
                        this.addMultiScopeDiagnostic({
1✔
696
                            ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
697
                            location: typeChainScan?.location
3!
698
                        });
699
                    }
700
                }
701

702
            } else {
703
                const typeChainScan = util.processTypeChain(typeChain);
222✔
704
                if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
222✔
705
                    this.addMultiScopeDiagnostic({
25✔
706
                        ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
707
                        location: typeChainScan?.location
75!
708
                    });
709
                } else {
710
                    this.addMultiScopeDiagnostic({
197✔
711
                        ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
712
                        location: typeChainScan?.location
591!
713
                    });
714
                }
715

716
            }
717
        }
718
        if (isUsedAsType) {
3,403✔
719
            return;
1,087✔
720
        }
721

722
        const containingNamespaceName = expression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
2,316✔
723

724
        if (!(isCallExpression(expression.parent) && isNewExpression(expression.parent?.parent))) {
2,316!
725
            const classUsedAsVarEntry = this.checkTypeChainForClassUsedAsVar(typeChain, containingNamespaceName);
2,203✔
726
            if (classUsedAsVarEntry) {
2,203!
727

UNCOV
728
                this.addMultiScopeDiagnostic({
×
729
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable(classUsedAsVarEntry.toString()),
730
                    location: expression.location
731
                });
UNCOV
732
                return;
×
733
            }
734
        }
735

736
        const lastTypeInfo = typeChain[typeChain.length - 1];
2,316✔
737
        const parentTypeInfo = typeChain[typeChain.length - 2];
2,316✔
738

739
        this.checkMemberAccessibility(file, expression, typeChain);
2,316✔
740

741
        if (isNamespaceType(exprType) && !isAliasStatement(expression.parent)) {
2,316✔
742
            this.addMultiScopeDiagnostic({
22✔
743
                ...DiagnosticMessages.itemCannotBeUsedAsVariable('namespace'),
744
                location: expression.location
745
            });
746
        } else if (isEnumType(exprType) && !isAliasStatement(expression.parent)) {
2,294✔
747
            const enumStatement = this.event.scope.getEnum(util.getAllDottedGetPartsAsString(expression));
15✔
748
            if (enumStatement) {
15✔
749
                // there's an enum with this name
750
                this.addMultiScopeDiagnostic({
2✔
751
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable('enum'),
752
                    location: expression.location
753
                });
754
            }
755
        } else if (isDynamicType(exprType) && isEnumType(parentTypeInfo?.type) && isDottedGetExpression(expression)) {
2,279✔
756
            const enumFileLink = this.event.scope.getEnumFileLink(util.getAllDottedGetPartsAsString(expression.obj));
8✔
757
            const typeChainScanForParent = util.processTypeChain(typeChain.slice(0, -1));
8✔
758
            if (enumFileLink) {
8✔
759
                this.addMultiScopeDiagnostic({
5✔
760
                    ...DiagnosticMessages.unknownEnumValue(lastTypeInfo?.name, typeChainScanForParent.fullChainName),
15!
761
                    location: lastTypeInfo?.location,
15!
762
                    relatedInformation: [{
763
                        message: 'Enum declared here',
764
                        location: util.createLocationFromRange(
765
                            URI.file(enumFileLink?.file.srcPath).toString(),
15!
766
                            enumFileLink?.item?.tokens.name.location?.range
45!
767
                        )
768
                    }]
769
                });
770
            }
771
        }
772
    }
773

774
    private checkTypeChainForClassUsedAsVar(typeChain: TypeChainEntry[], containingNamespaceName: string) {
775
        const ignoreKinds = [AstNodeKind.TypecastExpression, AstNodeKind.NewExpression];
2,203✔
776
        let lowerNameSoFar = '';
2,203✔
777
        let classUsedAsVar;
778
        let isFirst = true;
2,203✔
779
        for (let i = 0; i < typeChain.length - 1; i++) { // do not look at final entry - we CAN use the constructor as a variable
2,203✔
780
            const tce = typeChain[i];
1,193✔
781
            lowerNameSoFar += `${lowerNameSoFar ? '.' : ''}${tce.name.toLowerCase()}`;
1,193✔
782
            if (!isNamespaceType(tce.type)) {
1,193✔
783
                if (isFirst && containingNamespaceName) {
538✔
784
                    lowerNameSoFar = `${containingNamespaceName.toLowerCase()}.${lowerNameSoFar}`;
59✔
785
                }
786
                if (!tce.astNode || ignoreKinds.includes(tce.astNode.kind)) {
538✔
787
                    break;
12✔
788
                } else if (isClassType(tce.type) && lowerNameSoFar.toLowerCase() === tce.type.name.toLowerCase()) {
526!
UNCOV
789
                    classUsedAsVar = tce.type;
×
790
                }
791
                break;
526✔
792
            }
793
            isFirst = false;
655✔
794
        }
795

796
        return classUsedAsVar;
2,203✔
797
    }
798

799
    /**
800
     * Adds diagnostics for accibility mismatches
801
     *
802
     * @param file file
803
     * @param expression containing expression
804
     * @param typeChain type chain to check
805
     * @returns true if member accesiibility is okay
806
     */
807
    private checkMemberAccessibility(file: BscFile, expression: Expression, typeChain: TypeChainEntry[]) {
808
        for (let i = 0; i < typeChain.length - 1; i++) {
2,354✔
809
            const parentChainItem = typeChain[i];
1,324✔
810
            const childChainItem = typeChain[i + 1];
1,324✔
811
            if (isClassType(parentChainItem.type)) {
1,324✔
812
                const containingClassStmt = expression.findAncestor<ClassStatement>(isClassStatement);
150✔
813
                const classStmtThatDefinesChildMember = childChainItem.data?.definingNode?.findAncestor<ClassStatement>(isClassStatement);
150!
814
                if (classStmtThatDefinesChildMember) {
150✔
815
                    const definingClassName = classStmtThatDefinesChildMember.getName(ParseMode.BrighterScript);
148✔
816
                    const inMatchingClassStmt = containingClassStmt?.getName(ParseMode.BrighterScript).toLowerCase() === parentChainItem.type.name.toLowerCase();
148✔
817
                    // eslint-disable-next-line no-bitwise
818
                    if (childChainItem.data.flags & SymbolTypeFlag.private) {
148✔
819
                        if (!inMatchingClassStmt || childChainItem.data.memberOfAncestor) {
15✔
820
                            this.addMultiScopeDiagnostic({
4✔
821
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
822
                                location: expression.location
823
                            });
824
                            // there's an error... don't worry about the rest of the chain
825
                            return false;
4✔
826
                        }
827
                    }
828

829
                    // eslint-disable-next-line no-bitwise
830
                    if (childChainItem.data.flags & SymbolTypeFlag.protected) {
144✔
831
                        const containingClassName = containingClassStmt?.getName(ParseMode.BrighterScript);
13✔
832
                        const containingNamespaceName = expression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
13✔
833
                        const ancestorClasses = this.event.scope.getClassHierarchy(containingClassName, containingNamespaceName).map(link => link.item);
13✔
834
                        const isSubClassOfDefiningClass = ancestorClasses.includes(classStmtThatDefinesChildMember);
13✔
835

836
                        if (!isSubClassOfDefiningClass) {
13✔
837
                            this.addMultiScopeDiagnostic({
5✔
838
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
839
                                location: expression.location
840
                            });
841
                            // there's an error... don't worry about the rest of the chain
842
                            return false;
5✔
843
                        }
844
                    }
845
                }
846

847
            }
848
        }
849
        return true;
2,345✔
850
    }
851

852
    /**
853
     * Find all "new" statements in the program,
854
     * and make sure we can find a class with that name
855
     */
856
    private validateNewExpression(file: BrsFile, newExpression: NewExpression) {
857
        const newExprType = this.getNodeTypeWrapper(file, newExpression, { flags: SymbolTypeFlag.typetime });
113✔
858
        if (isClassType(newExprType)) {
113✔
859
            return;
105✔
860
        }
861

862
        let potentialClassName = newExpression.className.getName(ParseMode.BrighterScript);
8✔
863
        const namespaceName = newExpression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
8!
864
        let newableClass = this.event.scope.getClass(potentialClassName, namespaceName);
8✔
865

866
        if (!newableClass) {
8!
867
            //try and find functions with this name.
868
            let fullName = util.getFullyQualifiedClassName(potentialClassName, namespaceName);
8✔
869

870
            this.addMultiScopeDiagnostic({
8✔
871
                ...DiagnosticMessages.expressionIsNotConstructable(fullName),
872
                location: newExpression.className.location
873
            });
874

875
        }
876
    }
877

878
    /**
879
     * Create diagnostics for any duplicate function declarations
880
     */
881
    private flagDuplicateFunctionDeclarations() {
882
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration });
1,538✔
883

884
        //for each list of callables with the same name
885
        for (let [lowerName, callableContainers] of this.event.scope.getCallableContainerMap()) {
1,538✔
886

887
            let globalCallables = [] as CallableContainer[];
115,470✔
888
            let nonGlobalCallables = [] as CallableContainer[];
115,470✔
889
            let ownCallables = [] as CallableContainer[];
115,470✔
890
            let ancestorNonGlobalCallables = [] as CallableContainer[];
115,470✔
891

892

893
            for (let container of callableContainers) {
115,470✔
894
                if (container.scope === this.event.program.globalScope) {
121,656✔
895
                    globalCallables.push(container);
119,964✔
896
                } else {
897
                    nonGlobalCallables.push(container);
1,692✔
898
                    if (container.scope === this.event.scope) {
1,692✔
899
                        ownCallables.push(container);
1,662✔
900
                    } else {
901
                        ancestorNonGlobalCallables.push(container);
30✔
902
                    }
903
                }
904
            }
905

906
            //add info diagnostics about child shadowing parent functions
907
            if (ownCallables.length > 0 && ancestorNonGlobalCallables.length > 0) {
115,470✔
908
                for (let container of ownCallables) {
24✔
909
                    //skip the init function (because every component will have one of those){
910
                    if (lowerName !== 'init') {
24✔
911
                        let shadowedCallable = ancestorNonGlobalCallables[ancestorNonGlobalCallables.length - 1];
23✔
912
                        if (!!shadowedCallable && shadowedCallable.callable.file === container.callable.file) {
23✔
913
                            //same file: skip redundant imports
914
                            continue;
20✔
915
                        }
916
                        this.addMultiScopeDiagnostic({
3✔
917
                            ...DiagnosticMessages.overridesAncestorFunction(
918
                                container.callable.name,
919
                                container.scope.name,
920
                                shadowedCallable.callable.file.destPath,
921
                                //grab the last item in the list, which should be the closest ancestor's version
922
                                shadowedCallable.scope.name
923
                            ),
924
                            location: util.createLocationFromFileRange(container.callable.file, container.callable.nameRange)
925
                        }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
926
                    }
927
                }
928
            }
929

930
            //add error diagnostics about duplicate functions in the same scope
931
            if (ownCallables.length > 1) {
115,470✔
932

933
                for (let callableContainer of ownCallables) {
5✔
934
                    let callable = callableContainer.callable;
10✔
935
                    const related = [];
10✔
936
                    for (const ownCallable of ownCallables) {
10✔
937
                        const thatNameRange = ownCallable.callable.nameRange;
20✔
938
                        if (ownCallable.callable.nameRange !== callable.nameRange) {
20✔
939
                            related.push({
10✔
940
                                message: `Function declared here`,
941
                                location: util.createLocationFromRange(
942
                                    URI.file(ownCallable.callable.file?.srcPath).toString(),
30!
943
                                    thatNameRange
944
                                )
945
                            });
946
                        }
947
                    }
948

949
                    this.addMultiScopeDiagnostic({
10✔
950
                        ...DiagnosticMessages.duplicateFunctionImplementation(callable.name),
951
                        location: util.createLocationFromFileRange(callable.file, callable.nameRange),
952
                        relatedInformation: related
953
                    }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
954
                }
955
            }
956
        }
957
    }
958

959
    /**
960
     * Verify that all of the scripts imported by each file in this scope actually exist, and have the correct case
961
     */
962
    private validateScriptImportPaths() {
963
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Imports });
1,538✔
964

965
        let scriptImports = this.event.scope.getOwnScriptImports();
1,538✔
966
        //verify every script import
967
        for (let scriptImport of scriptImports) {
1,538✔
968
            let referencedFile = this.event.scope.getFileByRelativePath(scriptImport.destPath);
492✔
969
            //if we can't find the file
970
            if (!referencedFile) {
492✔
971
                //skip the default bslib file, it will exist at transpile time but should not show up in the program during validation cycle
972
                if (scriptImport.destPath === this.event.program.bslibPkgPath) {
15✔
973
                    continue;
2✔
974
                }
975
                let dInfo: DiagnosticInfo;
976
                if (scriptImport.text.trim().length === 0) {
13✔
977
                    dInfo = DiagnosticMessages.scriptSrcCannotBeEmpty();
1✔
978
                } else {
979
                    dInfo = DiagnosticMessages.referencedFileDoesNotExist();
12✔
980
                }
981

982
                this.addMultiScopeDiagnostic({
13✔
983
                    ...dInfo,
984
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
985
                }, ScopeValidatorDiagnosticTag.Imports);
986
                //if the character casing of the script import path does not match that of the actual path
987
            } else if (scriptImport.destPath !== referencedFile.destPath) {
477✔
988
                this.addMultiScopeDiagnostic({
2✔
989
                    ...DiagnosticMessages.scriptImportCaseMismatch(referencedFile.destPath),
990
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
991
                }, ScopeValidatorDiagnosticTag.Imports);
992
            }
993
        }
994
    }
995

996
    /**
997
     * Validate all classes defined in this scope
998
     */
999
    private validateClasses() {
1000
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Classes });
1,538✔
1001

1002
        let validator = new BsClassValidator(this.event.scope);
1,538✔
1003
        validator.validate();
1,538✔
1004
        for (const diagnostic of validator.diagnostics) {
1,538✔
1005
            this.addMultiScopeDiagnostic({
29✔
1006
                ...diagnostic
1007
            }, ScopeValidatorDiagnosticTag.Classes);
1008
        }
1009
    }
1010

1011

1012
    /**
1013
     * Find various function collisions
1014
     */
1015
    private diagnosticDetectFunctionCollisions(file: BrsFile) {
1016
        const fileUri = util.pathToUri(file.srcPath);
1,765✔
1017
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, tag: ScopeValidatorDiagnosticTag.FunctionCollisions });
1,765✔
1018
        for (let func of file.callables) {
1,765✔
1019
            const funcName = func.getName(ParseMode.BrighterScript);
1,531✔
1020
            const lowerFuncName = funcName?.toLowerCase();
1,531!
1021
            if (lowerFuncName) {
1,531!
1022

1023
                //find function declarations with the same name as a stdlib function
1024
                if (globalCallableMap.has(lowerFuncName)) {
1,531✔
1025
                    this.addMultiScopeDiagnostic({
5✔
1026
                        ...DiagnosticMessages.scopeFunctionShadowedByBuiltInFunction(),
1027
                        location: util.createLocationFromRange(fileUri, func.nameRange)
1028

1029
                    });
1030
                }
1031
            }
1032
        }
1033
    }
1034

1035
    public detectShadowedLocalVar(file: BrsFile, varDeclaration: { name: string; type: BscType; nameRange: Range }) {
1036
        const varName = varDeclaration.name;
1,616✔
1037
        const lowerVarName = varName.toLowerCase();
1,616✔
1038
        const callableContainerMap = this.event.scope.getCallableContainerMap();
1,616✔
1039

1040
        const varIsFunction = () => {
1,616✔
1041
            return isCallableType(varDeclaration.type);
11✔
1042
        };
1043

1044
        if (
1,616✔
1045
            //has same name as stdlib
1046
            globalCallableMap.has(lowerVarName)
1047
        ) {
1048
            //local var function with same name as stdlib function
1049
            if (varIsFunction()) {
8✔
1050
                this.addMultiScopeDiagnostic({
1✔
1051
                    ...DiagnosticMessages.localVarFunctionShadowsParentFunction('stdlib'),
1052
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange)
1053
                });
1054
            }
1055
        } else if (callableContainerMap.has(lowerVarName)) {
1,608✔
1056
            const callable = callableContainerMap.get(lowerVarName);
3✔
1057
            //is same name as a callable
1058
            if (varIsFunction()) {
3✔
1059
                this.addMultiScopeDiagnostic({
1✔
1060
                    ...DiagnosticMessages.localVarFunctionShadowsParentFunction('scope'),
1061
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1062
                    relatedInformation: [{
1063
                        message: 'Function declared here',
1064
                        location: util.createLocationFromFileRange(
1065
                            callable[0].callable.file,
1066
                            callable[0].callable.nameRange
1067
                        )
1068
                    }]
1069
                });
1070
            } else {
1071
                this.addMultiScopeDiagnostic({
2✔
1072
                    ...DiagnosticMessages.localVarShadowedByScopedFunction(),
1073
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1074
                    relatedInformation: [{
1075
                        message: 'Function declared here',
1076
                        location: util.createLocationFromRange(
1077
                            URI.file(callable[0].callable.file.srcPath).toString(),
1078
                            callable[0].callable.nameRange
1079
                        )
1080
                    }]
1081
                });
1082
            }
1083
            //has the same name as an in-scope class
1084
        } else {
1085
            const classStmtLink = this.event.scope.getClassFileLink(lowerVarName);
1,605✔
1086
            if (classStmtLink) {
1,605✔
1087
                this.addMultiScopeDiagnostic({
3✔
1088
                    ...DiagnosticMessages.localVarSameNameAsClass(classStmtLink?.item?.getName(ParseMode.BrighterScript)),
18!
1089
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1090
                    relatedInformation: [{
1091
                        message: 'Class declared here',
1092
                        location: util.createLocationFromRange(
1093
                            URI.file(classStmtLink.file.srcPath).toString(),
1094
                            classStmtLink?.item.tokens.name.location?.range
18!
1095
                        )
1096
                    }]
1097
                });
1098
            }
1099
        }
1100
    }
1101

1102
    private detectVariableNamespaceCollisions(file: BrsFile) {
1103
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.getFileUri(file), tag: ScopeValidatorDiagnosticTag.NamespaceCollisions });
1,908✔
1104

1105
        //find all function parameters
1106
        // eslint-disable-next-line @typescript-eslint/dot-notation
1107
        for (let func of file['_cachedLookups'].functionExpressions) {
1,908✔
1108
            for (let param of func.parameters) {
1,695✔
1109
                let lowerParamName = param.tokens.name.text.toLowerCase();
952✔
1110
                let namespace = this.event.scope.getNamespace(lowerParamName, param.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript).toLowerCase());
952✔
1111
                //see if the param matches any starting namespace part
1112
                if (namespace) {
952✔
1113
                    this.addMultiScopeDiagnostic({
4✔
1114
                        ...DiagnosticMessages.parameterMayNotHaveSameNameAsNamespace(param.tokens.name.text),
1115
                        location: param.tokens.name.location,
1116
                        relatedInformation: [{
1117
                            message: 'Namespace declared here',
1118
                            location: util.createLocationFromRange(
1119
                                URI.file(namespace.file.srcPath).toString(),
1120
                                namespace.nameRange
1121
                            )
1122
                        }]
1123
                    }, ScopeValidatorDiagnosticTag.NamespaceCollisions);
1124
                }
1125
            }
1126
        }
1127

1128
        // eslint-disable-next-line @typescript-eslint/dot-notation
1129
        for (let assignment of file['_cachedLookups'].assignmentStatements) {
1,908✔
1130
            let lowerAssignmentName = assignment.tokens.name.text.toLowerCase();
593✔
1131
            let namespace = this.event.scope.getNamespace(lowerAssignmentName, assignment.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript).toLowerCase());
593✔
1132
            //see if the param matches any starting namespace part
1133
            if (namespace) {
593✔
1134
                this.addMultiScopeDiagnostic({
4✔
1135
                    ...DiagnosticMessages.variableMayNotHaveSameNameAsNamespace(assignment.tokens.name.text),
1136
                    location: assignment.tokens.name.location,
1137
                    relatedInformation: [{
1138
                        message: 'Namespace declared here',
1139
                        location: util.createLocationFromRange(
1140
                            URI.file(namespace.file.srcPath).toString(),
1141
                            namespace.nameRange
1142
                        )
1143
                    }]
1144
                }, ScopeValidatorDiagnosticTag.NamespaceCollisions);
1145
            }
1146
        }
1147
    }
1148

1149
    private validateXmlInterface(scope: XmlScope) {
1150
        if (!scope.xmlFile.parser.ast?.componentElement?.interfaceElement) {
428!
1151
            return;
397✔
1152
        }
1153
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.getFileUri(scope.xmlFile), tag: ScopeValidatorDiagnosticTag.XMLInterface });
31✔
1154

1155
        const iface = scope.xmlFile.parser.ast.componentElement.interfaceElement;
31✔
1156
        const callableContainerMap = scope.getCallableContainerMap();
31✔
1157
        //validate functions
1158
        for (const func of iface.functions) {
31✔
1159
            const name = func.name;
33✔
1160
            if (!name) {
33✔
1161
                this.addDiagnostic({
3✔
1162
                    ...DiagnosticMessages.xmlTagMissingAttribute(func.tokens.startTagName.text, 'name'),
1163
                    location: func.tokens.startTagName.location
1164
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1165
            } else if (!callableContainerMap.has(name.toLowerCase())) {
30✔
1166
                this.addDiagnostic({
4✔
1167
                    ...DiagnosticMessages.xmlFunctionNotFound(name),
1168
                    location: func.getAttribute('name')?.tokens.value.location
12!
1169
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1170
            }
1171
        }
1172
        //validate fields
1173
        for (const field of iface.fields) {
31✔
1174
            const { id, type, onChange } = field;
28✔
1175
            if (!id) {
28✔
1176
                this.addDiagnostic({
3✔
1177
                    ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'id'),
1178
                    location: field.tokens.startTagName.location
1179
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1180
            }
1181
            if (!type) {
28✔
1182
                if (!field.alias) {
3✔
1183
                    this.addDiagnostic({
2✔
1184
                        ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'type'),
1185
                        location: field.tokens.startTagName.location
1186
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1187
                }
1188
            } else if (!SGFieldTypes.includes(type.toLowerCase())) {
25✔
1189
                this.addDiagnostic({
1✔
1190
                    ...DiagnosticMessages.xmlInvalidFieldType(type),
1191
                    location: field.getAttribute('type')?.tokens.value.location
3!
1192
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1193
            }
1194
            if (onChange) {
28✔
1195
                if (!callableContainerMap.has(onChange.toLowerCase())) {
1!
1196
                    this.addDiagnostic({
1✔
1197
                        ...DiagnosticMessages.xmlFunctionNotFound(onChange),
1198
                        location: field.getAttribute('onchange')?.tokens.value.location
3!
1199
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1200
                }
1201
            }
1202
        }
1203
    }
1204

1205
    /**
1206
     * Detect when a child has imported a script that an ancestor also imported
1207
     */
1208
    private diagnosticDetectDuplicateAncestorScriptImports(scope: XmlScope) {
1209
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.getFileUri(scope.xmlFile), tag: ScopeValidatorDiagnosticTag.XMLImports });
428✔
1210
        if (scope.xmlFile.parentComponent) {
428✔
1211
            //build a lookup of pkg paths -> FileReference so we can more easily look up collisions
1212
            let parentScriptImports = scope.xmlFile.getAncestorScriptTagImports();
34✔
1213
            let lookup = {} as Record<string, FileReference>;
34✔
1214
            for (let parentScriptImport of parentScriptImports) {
34✔
1215
                //keep the first occurance of a pkgPath. Parent imports are first in the array
1216
                if (!lookup[parentScriptImport.destPath]) {
30!
1217
                    lookup[parentScriptImport.destPath] = parentScriptImport;
30✔
1218
                }
1219
            }
1220

1221
            //add warning for every script tag that this file shares with an ancestor
1222
            for (let scriptImport of scope.xmlFile.scriptTagImports) {
34✔
1223
                let ancestorScriptImport = lookup[scriptImport.destPath];
30✔
1224
                if (ancestorScriptImport) {
30✔
1225
                    let ancestorComponent = ancestorScriptImport.sourceFile as XmlFile;
21✔
1226
                    let ancestorComponentName = ancestorComponent.componentName?.text ?? ancestorComponent.destPath;
21!
1227
                    this.addDiagnostic({
21✔
1228
                        location: util.createLocationFromFileRange(scope.xmlFile, scriptImport.filePathRange),
1229
                        ...DiagnosticMessages.unnecessaryScriptImportInChildFromParent(ancestorComponentName)
1230
                    }, ScopeValidatorDiagnosticTag.XMLImports);
1231
                }
1232
            }
1233
        }
1234
    }
1235

1236
    /**
1237
     * Wraps the AstNode.getType() method, so that we can do extra processing on the result based on the current file
1238
     * In particular, since BrightScript does not support Unions, and there's no way to cast them to something else
1239
     * if the result of .getType() is a union, and we're in a .brs (brightScript) file, treat the result as Dynamic
1240
     *
1241
     * In most cases, this returns the result of node.getType()
1242
     *
1243
     * @param file the current file being processed
1244
     * @param node the node to get the type of
1245
     * @param getTypeOpts any options to pass to node.getType()
1246
     * @returns the processed result type
1247
     */
1248
    private getNodeTypeWrapper(file: BrsFile, node: AstNode, getTypeOpts: GetTypeOptions) {
1249
        const type = node?.getType(getTypeOpts);
7,678✔
1250

1251
        if (file.parseMode === ParseMode.BrightScript) {
7,678✔
1252
            // this is a brightscript file
1253
            const typeChain = getTypeOpts.typeChain;
883✔
1254
            if (typeChain) {
883✔
1255
                const hasUnion = typeChain.reduce((hasUnion, tce) => {
311✔
1256
                    return hasUnion || isUnionType(tce.type);
355✔
1257
                }, false);
1258
                if (hasUnion) {
311✔
1259
                    // there was a union somewhere in the typechain
1260
                    return DynamicType.instance;
6✔
1261
                }
1262
            }
1263
            if (isUnionType(type)) {
877✔
1264
                //this is a union
1265
                return DynamicType.instance;
4✔
1266
            }
1267
        }
1268

1269
        // by default return the result of node.getType()
1270
        return type;
7,668✔
1271
    }
1272

1273
    private getParentTypeDescriptor(typeChainResult: TypeChainProcessResult) {
1274
        if (typeChainResult.itemParentTypeKind === BscTypeKind.NamespaceType) {
231✔
1275
            return 'namespace';
117✔
1276
        }
1277
        return 'type';
114✔
1278
    }
1279

1280
    private addDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1281
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
51!
1282
        this.event.program.diagnostics.register(diagnostic, {
51✔
1283
            tags: [diagnosticTag],
1284
            segment: this.currentSegmentBeingValidated
1285
        });
1286
    }
1287

1288
    /**
1289
     * Add a diagnostic (to the first scope) that will have `relatedInformation` for each affected scope
1290
     */
1291
    private addMultiScopeDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1292
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
450✔
1293
        this.event.program.diagnostics.register(diagnostic, {
450✔
1294
            tags: [diagnosticTag],
1295
            segment: this.currentSegmentBeingValidated,
1296
            scope: this.event.scope
1297
        });
1298
    }
1299
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc