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

rokucommunity / brighterscript / #12853

25 Jul 2024 01:32PM UTC coverage: 86.202%. Remained the same
#12853

push

web-flow
Merge 242a1aefa into 5f3ffa3fa

10593 of 13078 branches covered (81.0%)

Branch coverage included in aggregate %.

91 of 97 new or added lines in 15 files covered. (93.81%)

309 existing lines in 18 files now uncovered.

12292 of 13470 relevant lines covered (91.25%)

26625.48 hits per line

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

91.07
/src/bscPlugin/validation/ScopeValidator.ts
1
import { DiagnosticTag, type Range } from 'vscode-languageserver';
1✔
2
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✔
3
import type { DiagnosticInfo } from '../../DiagnosticMessages';
4
import { DiagnosticMessages } from '../../DiagnosticMessages';
1✔
5
import type { BrsFile } from '../../files/BrsFile';
6
import type { BsDiagnostic, CallableContainer, ExtraSymbolData, FileReference, GetTypeOptions, OnScopeValidateEvent, TypeChainEntry, TypeChainProcessResult, TypeCompatibilityData } from '../../interfaces';
7
import { SymbolTypeFlag } from '../../SymbolTypeFlag';
1✔
8
import type { AssignmentStatement, AugmentedAssignmentStatement, ClassStatement, DottedSetStatement, IncrementStatement, NamespaceStatement, ReturnStatement } from '../../parser/Statement';
9
import { util } from '../../util';
1✔
10
import { nodes, components } from '../../roku-types';
1✔
11
import type { BRSComponentData } from '../../roku-types';
12
import type { Token } from '../../lexer/Token';
13
import { AstNodeKind } from '../../parser/AstNode';
1✔
14
import type { AstNode } from '../../parser/AstNode';
15
import type { Expression } from '../../parser/AstNode';
16
import type { VariableExpression, DottedGetExpression, BinaryExpression, UnaryExpression, NewExpression, LiteralExpression } from '../../parser/Expression';
17
import { CallExpression } from '../../parser/Expression';
1✔
18
import { createVisitor } from '../../astUtils/visitors';
1✔
19
import type { BscType } from '../../types/BscType';
20
import type { BscFile } from '../../files/BscFile';
21
import { InsideSegmentWalkMode } from '../../AstValidationSegmenter';
1✔
22
import { TokenKind } from '../../lexer/TokenKind';
1✔
23
import { ParseMode } from '../../parser/Parser';
1✔
24
import { BsClassValidator } from '../../validators/ClassValidator';
1✔
25
import { globalCallableMap } from '../../globalCallables';
1✔
26
import type { XmlScope } from '../../XmlScope';
27
import type { XmlFile } from '../../files/XmlFile';
28
import { SGFieldTypes } from '../../parser/SGTypes';
1✔
29
import { DynamicType } from '../../types';
1✔
30
import { BscTypeKind } from '../../types/BscTypeKind';
1✔
31

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

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

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

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

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

65

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

225
    private currentSegmentBeingValidated: AstNode;
226

227

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

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

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

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

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

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

327
    }
328

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

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

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

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

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

395
        }
396
    }
397

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

405

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

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

422
            }
423
        }
424
    }
425

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

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

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

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

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

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

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

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

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

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

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

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

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

535

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

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

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

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

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

571

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

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

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

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

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

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

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

622

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

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

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

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

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

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

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

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

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

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

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

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

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

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

773
    private checkTypeChainForClassUsedAsVar(typeChain: TypeChainEntry[], containingNamespaceName: string) {
774
        const ignoreKinds = [AstNodeKind.TypecastExpression, AstNodeKind.NewExpression];
2,203✔
775
        let lowerNameSoFar = '';
2,203✔
776
        let classUsedAsVar;
777
        let isFirst = true;
2,203✔
778
        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✔
779
            const tce = typeChain[i];
1,193✔
780
            lowerNameSoFar += `${lowerNameSoFar ? '.' : ''}${tce.name.toLowerCase()}`;
1,193✔
781
            if (!isNamespaceType(tce.type)) {
1,193✔
782
                if (isFirst && containingNamespaceName) {
538✔
783
                    lowerNameSoFar = `${containingNamespaceName.toLowerCase()}.${lowerNameSoFar}`;
59✔
784
                }
785
                if (!tce.astNode || ignoreKinds.includes(tce.astNode.kind)) {
538✔
786
                    break;
12✔
787
                } else if (isClassType(tce.type) && lowerNameSoFar.toLowerCase() === tce.type.name.toLowerCase()) {
526!
UNCOV
788
                    classUsedAsVar = tce.type;
×
789
                }
790
                break;
526✔
791
            }
792
            isFirst = false;
655✔
793
        }
794

795
        return classUsedAsVar;
2,203✔
796
    }
797

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

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

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

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

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

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

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

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

874
        }
875
    }
876

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

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

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

891

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

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

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

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

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

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

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

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

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

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

1010

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

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

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

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

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

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

1101
    private detectVariableNamespaceCollisions(file: BrsFile) {
1102
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(file?.srcPath), tag: ScopeValidatorDiagnosticTag.NamespaceCollisions });
1,908!
1103

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

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

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

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

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

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

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

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

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

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

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

1287
    /**
1288
     * Add a diagnostic (to the first scope) that will have `relatedInformation` for each affected scope
1289
     */
1290
    private addMultiScopeDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1291
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
450✔
1292
        this.event.program.diagnostics.register(diagnostic, {
450✔
1293
            tags: [diagnosticTag],
1294
            segment: this.currentSegmentBeingValidated,
1295
            scope: this.event.scope
1296
        });
1297
    }
1298
}
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