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

rokucommunity / brighterscript / #12855

25 Jul 2024 05:41PM UTC coverage: 85.626%. Remained the same
#12855

push

web-flow
Merge 7c29dfd7b into 5f3ffa3fa

10816 of 13510 branches covered (80.06%)

Branch coverage included in aggregate %.

9 of 9 new or added lines in 2 files covered. (100.0%)

37 existing lines in 2 files now uncovered.

12279 of 13462 relevant lines covered (91.21%)

26652.67 hits per line

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

88.46
/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,536✔
65

66

67
    public processEvent(event: OnScopeValidateEvent) {
68
        this.event = event;
3,077✔
69
        if (this.event.program.globalScope === this.event.scope) {
3,077✔
70
            return;
1,536✔
71
        }
72
        this.metrics.clear();
1,541✔
73
        this.walkFiles();
1,541✔
74
        this.currentSegmentBeingValidated = null;
1,541✔
75
        this.flagDuplicateFunctionDeclarations();
1,541✔
76
        this.validateScriptImportPaths();
1,541✔
77
        this.validateClasses();
1,541✔
78
        if (isXmlScope(event.scope)) {
1,541✔
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,541✔
86
        let total = 0;
1,541✔
87
        for (const [filePath, num] of this.metrics) {
1,541✔
88
            this.event.program.logger.debug(' - ', filePath, num);
1,503✔
89
            total += num;
1,503✔
90
        }
91
        this.event.program.logger.debug(this.event.scope.name, 'total segments validated', total);
1,541✔
92
    }
93

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

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

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

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

109
            if (thisFileHasChanges || this.doesFileProvideChangedSymbol(file, this.event.changedSymbols)) {
1,911✔
110
                this.diagnosticDetectFunctionCollisions(file);
1,768✔
111
            }
112
        });
113

114
        this.event.scope.enumerateOwnFiles((file) => {
1,541✔
115
            if (isBrsFile(file)) {
2,329✔
116
                const thisFileHasChanges = this.event.changedFiles.includes(file);
1,901✔
117

118
                const hasUnvalidatedSegments = file.validationSegmenter.hasUnvalidatedSegments();
1,901✔
119

120
                if (hasChangeInfo && !hasUnvalidatedSegments) {
1,901✔
121
                    return;
398✔
122
                }
123

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

182
                const segmentsToWalkForValidation = thisFileHasChanges
1,503✔
183
                    ? file.validationSegmenter.getAllUnvalidatedSegments()
1,503✔
184
                    : file.validationSegmenter.getSegmentsWithChangedSymbols(this.event.changedSymbols);
185

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

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

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

222
    private currentSegmentBeingValidated: AstNode;
223

224

225
    private isTypeKnown(exprType: BscType) {
226
        let isKnownType = exprType?.isResolvable();
3,408✔
227
        return isKnownType;
3,408✔
228
    }
229

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

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

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

271
        //if this is a `createObject('roSGNode'` call, only support known sg node types
272
        if (firstParamStringValueLower === 'rosgnode' && isLiteralExpression(call?.args[1])) {
61!
273
            const componentName: Token = call?.args[1]?.tokens.value;
25!
274
            //don't validate any components with a colon in their name (probably component libraries, but regular components can have them too).
275
            if (!componentName || componentName?.text?.includes(':')) {
25!
276
                return;
3✔
277
            }
278
            //add diagnostic for unknown components
279
            const unquotedComponentName = componentName?.text?.replace(/"/g, '');
22!
280
            if (unquotedComponentName && !platformNodeNames.has(unquotedComponentName.toLowerCase()) && !this.event.program.getComponent(unquotedComponentName)) {
22✔
281
                this.addDiagnostic({
4✔
282
                    file: file as BscFile,
283
                    ...DiagnosticMessages.unknownRoSGNode(unquotedComponentName),
284
                    range: componentName.location?.range
12!
285
                });
286
            } else if (call?.args.length !== 2) {
18!
287
                // roSgNode should only ever have 2 args in `createObject`
288
                this.addDiagnostic({
1✔
289
                    file: file as BscFile,
290
                    ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, [2], call?.args.length),
3!
291
                    range: call.location?.range
3!
292
                });
293
            }
294
        } else if (!platformComponentNames.has(firstParamStringValueLower)) {
36✔
295
            this.addDiagnostic({
7✔
296
                file: file as BscFile,
297
                ...DiagnosticMessages.unknownBrightScriptComponent(firstParamStringValue),
298
                range: firstParamToken.location?.range
21!
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
                    file: file as BscFile,
314
                    ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, validArgCounts, call?.args.length),
12!
315
                    range: call.location?.range
12!
316
                });
317
            }
318

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

329
    }
330

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

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

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

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

388
                const compatibilityData: TypeCompatibilityData = {};
505✔
389
                const isAllowedArgConversion = this.checkAllowedArgConversions(paramType, argType);
505✔
390
                if (!isAllowedArgConversion && !paramType?.isTypeCompatible(argType, compatibilityData)) {
505!
391
                    this.addMultiScopeDiagnostic({
32✔
392
                        ...DiagnosticMessages.argumentTypeMismatch(argType.toString(), paramType.toString(), compatibilityData),
393
                        range: arg.location?.range,
96!
394
                        //TODO detect end of expression call
395
                        file: file
396
                    });
397
                }
398
                paramIndex++;
505✔
399
            }
400

401
        }
402
    }
403

404
    private checkAllowedArgConversions(paramType: BscType, argType: BscType): boolean {
405
        if (isNumberType(argType) && isBooleanType(paramType)) {
505✔
406
            return true;
8✔
407
        }
408
        return false;
497✔
409
    }
410

411

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

422
            if (actualReturnType && !funcType.returnType.isTypeCompatible(actualReturnType, compatibilityData)) {
247✔
423
                this.addMultiScopeDiagnostic({
12✔
424
                    ...DiagnosticMessages.returnTypeMismatch(actualReturnType.toString(), funcType.returnType.toString(), compatibilityData),
425
                    range: returnStmt.value.location?.range,
36!
426
                    file: file
427
                });
428

429
            }
430
        }
431
    }
432

433
    /**
434
     * Detect assigned type different from expected member type
435
     */
436
    private validateDottedSetStatement(file: BrsFile, dottedSetStmt: DottedSetStatement) {
437
        const typeChainExpectedLHS = [] as TypeChainEntry[];
73✔
438
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
73✔
439

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

459
        let accessibilityIsOk = this.checkMemberAccessibility(file, dottedSetStmt, typeChainExpectedLHS);
38✔
460

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

470
        if (accessibilityIsOk && !expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
34!
471
            this.addMultiScopeDiagnostic({
7✔
472
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
473
                range: dottedSetStmt.location?.range,
21!
474
                file: file
475
            });
476
        }
477
    }
478

479
    /**
480
     * Detect when declared type does not match rhs type
481
     */
482
    private validateAssignmentStatement(file: BrsFile, assignStmt: AssignmentStatement) {
483
        if (!assignStmt?.typeExpression) {
578!
484
            // nothing to check
485
            return;
572✔
486
        }
487

488
        const typeChainExpectedLHS = [];
6✔
489
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
6✔
490
        const expectedLHSType = this.getNodeTypeWrapper(file, assignStmt.typeExpression, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
6✔
491
        const actualRHSType = this.getNodeTypeWrapper(file, assignStmt.value, getTypeOpts);
6✔
492
        const compatibilityData: TypeCompatibilityData = {};
6✔
493
        if (!expectedLHSType || !expectedLHSType.isResolvable()) {
6✔
494
            // LHS is not resolvable... handled elsewhere
495
        } else if (!expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
5!
496
            this.addMultiScopeDiagnostic({
1✔
497
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
498
                range: assignStmt.location?.range,
3!
499
                file: file
500
            });
501
        }
502
    }
503

504
    /**
505
     * Detect invalid use of a binary operator
506
     */
507
    private validateBinaryExpression(file: BrsFile, binaryExpr: BinaryExpression | AugmentedAssignmentStatement) {
508
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
271✔
509

510
        if (util.isInTypeExpression(binaryExpr)) {
271✔
511
            return;
13✔
512
        }
513

514
        let leftType = isBinaryExpression(binaryExpr)
258✔
515
            ? this.getNodeTypeWrapper(file, binaryExpr.left, getTypeOpts)
258✔
516
            : this.getNodeTypeWrapper(file, binaryExpr.item, getTypeOpts);
517
        let rightType = isBinaryExpression(binaryExpr)
258✔
518
            ? this.getNodeTypeWrapper(file, binaryExpr.right, getTypeOpts)
258✔
519
            : this.getNodeTypeWrapper(file, binaryExpr.value, getTypeOpts);
520

521
        if (!leftType.isResolvable() || !rightType.isResolvable()) {
258✔
522
            // Can not find the type. error handled elsewhere
523
            return;
10✔
524
        }
525
        let leftTypeToTest = leftType;
248✔
526
        let rightTypeToTest = rightType;
248✔
527

528
        if (isEnumMemberType(leftType) || isEnumType(leftType)) {
248✔
529
            leftTypeToTest = leftType.underlyingType;
11✔
530
        }
531
        if (isEnumMemberType(rightType) || isEnumType(rightType)) {
248✔
532
            rightTypeToTest = rightType.underlyingType;
10✔
533
        }
534

535
        if (isUnionType(leftType) || isUnionType(rightType)) {
248!
536
            // TODO: it is possible to validate based on innerTypes, but more complicated
537
            // Because you need to verify each combination of types
UNCOV
538
            return;
×
539
        }
540
        const leftIsPrimitive = isPrimitiveType(leftTypeToTest);
248✔
541
        const rightIsPrimitive = isPrimitiveType(rightTypeToTest);
248✔
542
        const leftIsAny = isDynamicType(leftTypeToTest) || isObjectType(leftTypeToTest);
248✔
543
        const rightIsAny = isDynamicType(rightTypeToTest) || isObjectType(rightTypeToTest);
248✔
544

545

546
        if (leftIsAny && rightIsAny) {
248✔
547
            // both operands are basically "any" type... ignore;
548
            return;
19✔
549
        } else if ((leftIsAny && rightIsPrimitive) || (leftIsPrimitive && rightIsAny)) {
229✔
550
            // one operand is basically "any" type... ignore;
551
            return;
42✔
552
        }
553
        const opResult = util.binaryOperatorResultType(leftTypeToTest, binaryExpr.tokens.operator, rightTypeToTest);
187✔
554

555
        if (isDynamicType(opResult)) {
187✔
556
            // if the result was dynamic, that means there wasn't a valid operation
557
            this.addMultiScopeDiagnostic({
7✔
558
                ...DiagnosticMessages.operatorTypeMismatch(binaryExpr.tokens.operator.text, leftType.toString(), rightType.toString()),
559
                range: binaryExpr.location?.range,
21!
560
                file: file
561
            });
562
        }
563
    }
564

565
    /**
566
     * Detect invalid use of a Unary operator
567
     */
568
    private validateUnaryExpression(file: BrsFile, unaryExpr: UnaryExpression) {
569
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
33✔
570

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

573
        if (!rightType.isResolvable()) {
33!
574
            // Can not find the type. error handled elsewhere
UNCOV
575
            return;
×
576
        }
577
        let rightTypeToTest = rightType;
33✔
578
        if (isEnumMemberType(rightType)) {
33!
UNCOV
579
            rightTypeToTest = rightType.underlyingType;
×
580
        }
581

582

583
        if (isUnionType(rightTypeToTest)) {
33✔
584
            // TODO: it is possible to validate based on innerTypes, but more complicated
585
            // Because you need to verify each combination of types
586

587
        } else if (isDynamicType(rightTypeToTest) || isObjectType(rightTypeToTest)) {
32✔
588
            // operand is basically "any" type... ignore;
589

590
        } else if (isPrimitiveType(rightType)) {
27!
591
            const opResult = util.unaryOperatorResultType(unaryExpr.tokens.operator, rightTypeToTest);
27✔
592
            if (isDynamicType(opResult)) {
27✔
593
                this.addMultiScopeDiagnostic({
1✔
594
                    ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
595
                    range: unaryExpr.location?.range,
3!
596
                    file: file
597
                });
598
            }
599
        } else {
600
            // rhs is not a primitive, so no binary operator is allowed
UNCOV
601
            this.addMultiScopeDiagnostic({
×
602
                ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
603
                range: unaryExpr.location?.range,
×
604
                file: file
605
            });
606
        }
607
    }
608

609
    private validateIncrementStatement(file: BrsFile, incStmt: IncrementStatement) {
610
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
9✔
611

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

614
        if (!rightType.isResolvable()) {
9!
615
            // Can not find the type. error handled elsewhere
UNCOV
616
            return;
×
617
        }
618

619
        if (isUnionType(rightType)) {
9!
620
            // TODO: it is possible to validate based on innerTypes, but more complicated
621
            // because you need to verify each combination of types
622
        } else if (isDynamicType(rightType) || isObjectType(rightType)) {
9✔
623
            // operand is basically "any" type... ignore
624
        } else if (isNumberType(rightType)) {
7✔
625
            // operand is a number.. this is ok
626
        } else {
627
            // rhs is not a number, so no increment operator is not allowed
628
            this.addMultiScopeDiagnostic({
1✔
629
                ...DiagnosticMessages.operatorTypeMismatch(incStmt.tokens.operator.text, rightType.toString()),
630
                range: incStmt.location?.range,
3!
631
                file: file
632
            });
633
        }
634
    }
635

636

637
    validateVariableAndDottedGetExpressions(file: BrsFile, expression: VariableExpression | DottedGetExpression) {
638
        if (isDottedGetExpression(expression.parent)) {
4,689✔
639
            // We validate dottedGetExpressions at the top-most level
640
            return;
1,278✔
641
        }
642
        if (isVariableExpression(expression)) {
3,411✔
643
            if (isAssignmentStatement(expression.parent) && expression.parent.tokens.name === expression.tokens.name) {
2,432!
644
                // Don't validate LHS of assignments
UNCOV
645
                return;
×
646
            } else if (isNamespaceStatement(expression.parent)) {
2,432✔
647
                return;
3✔
648
            }
649
        }
650

651
        let symbolType = SymbolTypeFlag.runtime;
3,408✔
652
        let oppositeSymbolType = SymbolTypeFlag.typetime;
3,408✔
653
        const isUsedAsType = util.isInTypeExpression(expression);
3,408✔
654
        if (isUsedAsType) {
3,408✔
655
            // This is used in a TypeExpression - only look up types from SymbolTable
656
            symbolType = SymbolTypeFlag.typetime;
1,088✔
657
            oppositeSymbolType = SymbolTypeFlag.runtime;
1,088✔
658
        }
659

660
        // Do a complete type check on all DottedGet and Variable expressions
661
        // this will create a diagnostic if an invalid member is accessed
662
        const typeChain: TypeChainEntry[] = [];
3,408✔
663
        const typeData = {} as ExtraSymbolData;
3,408✔
664
        let exprType = this.getNodeTypeWrapper(file, expression, {
3,408✔
665
            flags: symbolType,
666
            typeChain: typeChain,
667
            data: typeData
668
        });
669

670
        const hasValidDeclaration = this.hasValidDeclaration(expression, exprType, typeData?.definingNode);
3,408!
671

672
        //include a hint diagnostic if this type is marked as deprecated
673
        if (typeData.flags & SymbolTypeFlag.deprecated) { // eslint-disable-line no-bitwise
3,408✔
674
            this.addMultiScopeDiagnostic({
2✔
675
                ...DiagnosticMessages.itemIsDeprecated(),
676
                range: expression.tokens.name.location?.range,
6!
677
                file: file,
678
                tags: [DiagnosticTag.Deprecated]
679
            });
680
        }
681

682
        if (!this.isTypeKnown(exprType) && !hasValidDeclaration) {
3,408✔
683
            if (this.getNodeTypeWrapper(file, expression, { flags: oppositeSymbolType, isExistenceTest: true })?.isResolvable()) {
227!
684
                const oppoSiteTypeChain = [];
5✔
685
                const invalidlyUsedResolvedType = this.getNodeTypeWrapper(file, expression, { flags: oppositeSymbolType, typeChain: oppoSiteTypeChain, isExistenceTest: true });
5✔
686
                const typeChainScan = util.processTypeChain(oppoSiteTypeChain);
5✔
687
                if (isUsedAsType) {
5✔
688
                    this.addMultiScopeDiagnostic({
2✔
689
                        ...DiagnosticMessages.itemCannotBeUsedAsType(typeChainScan.fullChainName),
690
                        range: expression.location?.range,
6!
691
                        file: file
692
                    });
693
                } else if (invalidlyUsedResolvedType && !isReferenceType(invalidlyUsedResolvedType)) {
3✔
694
                    if (!isAliasStatement(expression.parent)) {
1!
695
                        // alias rhs CAN be a type!
UNCOV
696
                        this.addMultiScopeDiagnostic({
×
697
                            ...DiagnosticMessages.itemCannotBeUsedAsVariable(invalidlyUsedResolvedType.toString()),
698
                            range: expression.location?.range,
×
699
                            file: file
700
                        });
701
                    }
702
                } else {
703
                    const typeChainScan = util.processTypeChain(typeChain);
2✔
704
                    //if this is a function call, provide a different diganostic code
705
                    if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
2✔
706
                        this.addMultiScopeDiagnostic({
1✔
707
                            file: file as BscFile,
708
                            ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
709
                            range: typeChainScan?.location?.range
6!
710
                        });
711
                    } else {
712
                        this.addMultiScopeDiagnostic({
1✔
713
                            file: file as BscFile,
714
                            ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
715
                            range: typeChainScan?.location?.range
6!
716
                        });
717
                    }
718
                }
719

720
            } else {
721
                const typeChainScan = util.processTypeChain(typeChain);
222✔
722
                if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
222✔
723
                    this.addMultiScopeDiagnostic({
25✔
724
                        file: file as BscFile,
725
                        ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
726
                        range: typeChainScan?.location?.range
150!
727
                    });
728
                } else {
729
                    this.addMultiScopeDiagnostic({
197✔
730
                        file: file as BscFile,
731
                        ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
732
                        range: typeChainScan?.location?.range
1,182!
733
                    });
734
                }
735

736
            }
737
        }
738
        if (isUsedAsType) {
3,408✔
739
            return;
1,088✔
740
        }
741

742
        const containingNamespaceName = expression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
2,320✔
743

744
        if (!(isCallExpression(expression.parent) && isNewExpression(expression.parent?.parent))) {
2,320!
745
            const classUsedAsVarEntry = this.checkTypeChainForClassUsedAsVar(typeChain, containingNamespaceName);
2,207✔
746
            if (classUsedAsVarEntry) {
2,207!
747

UNCOV
748
                this.addMultiScopeDiagnostic({
×
749
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable(classUsedAsVarEntry.toString()),
750
                    range: expression.location?.range,
×
751
                    file: file
752
                });
UNCOV
753
                return;
×
754
            }
755
        }
756

757
        const lastTypeInfo = typeChain[typeChain.length - 1];
2,320✔
758
        const parentTypeInfo = typeChain[typeChain.length - 2];
2,320✔
759

760
        this.checkMemberAccessibility(file, expression, typeChain);
2,320✔
761

762
        if (isNamespaceType(exprType) && !isAliasStatement(expression.parent)) {
2,320✔
763
            this.addMultiScopeDiagnostic({
22✔
764
                ...DiagnosticMessages.itemCannotBeUsedAsVariable('namespace'),
765
                range: expression.location?.range,
66!
766
                file: file
767
            });
768
        } else if (isEnumType(exprType) && !isAliasStatement(expression.parent)) {
2,298✔
769
            const enumStatement = this.event.scope.getEnum(util.getAllDottedGetPartsAsString(expression));
15✔
770
            if (enumStatement) {
15✔
771
                // there's an enum with this name
772
                this.addMultiScopeDiagnostic({
2✔
773
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable('enum'),
774
                    range: expression.location?.range,
6!
775
                    file: file
776
                });
777
            }
778
        } else if (isDynamicType(exprType) && isEnumType(parentTypeInfo?.type) && isDottedGetExpression(expression)) {
2,283✔
779
            const enumFileLink = this.event.scope.getEnumFileLink(util.getAllDottedGetPartsAsString(expression.obj));
8✔
780
            const typeChainScanForParent = util.processTypeChain(typeChain.slice(0, -1));
8✔
781
            if (enumFileLink) {
8✔
782
                this.addMultiScopeDiagnostic({
5✔
783
                    file: file,
784
                    ...DiagnosticMessages.unknownEnumValue(lastTypeInfo?.name, typeChainScanForParent.fullChainName),
15!
785
                    range: lastTypeInfo?.location?.range,
30!
786
                    relatedInformation: [{
787
                        message: 'Enum declared here',
788
                        location: util.createLocationFromRange(
789
                            URI.file(enumFileLink?.file.srcPath).toString(),
15!
790
                            enumFileLink?.item?.tokens.name.location?.range
45!
791
                        )
792
                    }]
793
                });
794
            }
795
        }
796
    }
797

798
    private checkTypeChainForClassUsedAsVar(typeChain: TypeChainEntry[], containingNamespaceName: string) {
799
        const ignoreKinds = [AstNodeKind.TypecastExpression, AstNodeKind.NewExpression];
2,207✔
800
        let lowerNameSoFar = '';
2,207✔
801
        let classUsedAsVar;
802
        let isFirst = true;
2,207✔
803
        for (let i = 0; i < typeChain.length - 1; i++) { // do not look at final entry - we CAN use the constructor as a variable
2,207✔
804
            const tce = typeChain[i];
1,197✔
805
            lowerNameSoFar += `${lowerNameSoFar ? '.' : ''}${tce.name.toLowerCase()}`;
1,197✔
806
            if (!isNamespaceType(tce.type)) {
1,197✔
807
                if (isFirst && containingNamespaceName) {
542✔
808
                    lowerNameSoFar = `${containingNamespaceName.toLowerCase()}.${lowerNameSoFar}`;
60✔
809
                }
810
                if (!tce.astNode || ignoreKinds.includes(tce.astNode.kind)) {
542✔
811
                    break;
12✔
812
                } else if (isClassType(tce.type) && lowerNameSoFar.toLowerCase() === tce.type.name.toLowerCase()) {
530!
UNCOV
813
                    classUsedAsVar = tce.type;
×
814
                }
815
                break;
530✔
816
            }
817
            isFirst = false;
655✔
818
        }
819

820
        return classUsedAsVar;
2,207✔
821
    }
822

823
    /**
824
     * Adds diagnostics for accibility mismatches
825
     *
826
     * @param file file
827
     * @param expression containing expression
828
     * @param typeChain type chain to check
829
     * @returns true if member accesiibility is okay
830
     */
831
    private checkMemberAccessibility(file: BscFile, expression: Expression, typeChain: TypeChainEntry[]) {
832
        for (let i = 0; i < typeChain.length - 1; i++) {
2,358✔
833
            const parentChainItem = typeChain[i];
1,328✔
834
            const childChainItem = typeChain[i + 1];
1,328✔
835
            if (isClassType(parentChainItem.type)) {
1,328✔
836
                const containingClassStmt = expression.findAncestor<ClassStatement>(isClassStatement);
150✔
837
                const classStmtThatDefinesChildMember = childChainItem.data?.definingNode?.findAncestor<ClassStatement>(isClassStatement);
150!
838
                if (classStmtThatDefinesChildMember) {
150✔
839
                    const definingClassName = classStmtThatDefinesChildMember.getName(ParseMode.BrighterScript);
148✔
840
                    const inMatchingClassStmt = containingClassStmt?.getName(ParseMode.BrighterScript).toLowerCase() === parentChainItem.type.name.toLowerCase();
148✔
841
                    // eslint-disable-next-line no-bitwise
842
                    if (childChainItem.data.flags & SymbolTypeFlag.private) {
148✔
843
                        if (!inMatchingClassStmt || childChainItem.data.memberOfAncestor) {
15✔
844
                            this.addMultiScopeDiagnostic({
4✔
845
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
846
                                range: expression.location?.range,
12!
847
                                file: file
848
                            });
849
                            // there's an error... don't worry about the rest of the chain
850
                            return false;
4✔
851
                        }
852
                    }
853

854
                    // eslint-disable-next-line no-bitwise
855
                    if (childChainItem.data.flags & SymbolTypeFlag.protected) {
144✔
856
                        const containingClassName = containingClassStmt?.getName(ParseMode.BrighterScript);
13✔
857
                        const containingNamespaceName = expression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
13✔
858
                        const ancestorClasses = this.event.scope.getClassHierarchy(containingClassName, containingNamespaceName).map(link => link.item);
13✔
859
                        const isSubClassOfDefiningClass = ancestorClasses.includes(classStmtThatDefinesChildMember);
13✔
860

861
                        if (!isSubClassOfDefiningClass) {
13✔
862
                            this.addMultiScopeDiagnostic({
5✔
863
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
864
                                range: expression.location?.range,
15!
865
                                file: file
866
                            });
867
                            // there's an error... don't worry about the rest of the chain
868
                            return false;
5✔
869
                        }
870
                    }
871
                }
872

873
            }
874
        }
875
        return true;
2,349✔
876
    }
877

878
    /**
879
     * Find all "new" statements in the program,
880
     * and make sure we can find a class with that name
881
     */
882
    private validateNewExpression(file: BrsFile, newExpression: NewExpression) {
883
        const newExprType = this.getNodeTypeWrapper(file, newExpression, { flags: SymbolTypeFlag.typetime });
113✔
884
        if (isClassType(newExprType)) {
113✔
885
            return;
105✔
886
        }
887

888
        let potentialClassName = newExpression.className.getName(ParseMode.BrighterScript);
8✔
889
        const namespaceName = newExpression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
8!
890
        let newableClass = this.event.scope.getClass(potentialClassName, namespaceName);
8✔
891

892
        if (!newableClass) {
8!
893
            //try and find functions with this name.
894
            let fullName = util.getFullyQualifiedClassName(potentialClassName, namespaceName);
8✔
895

896
            this.addMultiScopeDiagnostic({
8✔
897
                ...DiagnosticMessages.expressionIsNotConstructable(fullName),
898
                file: file,
899
                range: newExpression.className.location?.range
24!
900
            });
901

902
        }
903
    }
904

905
    /**
906
     * Create diagnostics for any duplicate function declarations
907
     */
908
    private flagDuplicateFunctionDeclarations() {
909
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration });
1,541✔
910

911
        //for each list of callables with the same name
912
        for (let [lowerName, callableContainers] of this.event.scope.getCallableContainerMap()) {
1,541✔
913

914
            let globalCallables = [] as CallableContainer[];
115,699✔
915
            let nonGlobalCallables = [] as CallableContainer[];
115,699✔
916
            let ownCallables = [] as CallableContainer[];
115,699✔
917
            let ancestorNonGlobalCallables = [] as CallableContainer[];
115,699✔
918

919
            for (let container of callableContainers) {
115,699✔
920
                if (container.scope === this.event.program.globalScope) {
121,897✔
921
                    globalCallables.push(container);
120,198✔
922
                } else {
923
                    nonGlobalCallables.push(container);
1,699✔
924
                    if (container.scope === this.event.scope) {
1,699✔
925
                        ownCallables.push(container);
1,669✔
926
                    } else {
927
                        ancestorNonGlobalCallables.push(container);
30✔
928
                    }
929
                }
930
            }
931

932
            //add info diagnostics about child shadowing parent functions
933
            if (ownCallables.length > 0 && ancestorNonGlobalCallables.length > 0) {
115,699✔
934
                for (let container of ownCallables) {
24✔
935
                    //skip the init function (because every component will have one of those){
936
                    if (lowerName !== 'init') {
24✔
937
                        let shadowedCallable = ancestorNonGlobalCallables[ancestorNonGlobalCallables.length - 1];
23✔
938
                        if (!!shadowedCallable && shadowedCallable.callable.file === container.callable.file) {
23✔
939
                            //same file: skip redundant imports
940
                            continue;
20✔
941
                        }
942
                        this.addMultiScopeDiagnostic({
3✔
943
                            ...DiagnosticMessages.overridesAncestorFunction(
944
                                container.callable.name,
945
                                container.scope.name,
946
                                shadowedCallable.callable.file.destPath,
947
                                //grab the last item in the list, which should be the closest ancestor's version
948
                                shadowedCallable.scope.name
949
                            ),
950
                            range: container.callable.nameRange,
951
                            file: container.callable.file
952
                        }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
953
                    }
954
                }
955
            }
956

957
            //add error diagnostics about duplicate functions in the same scope
958
            if (ownCallables.length > 1) {
115,699✔
959

960
                for (let callableContainer of ownCallables) {
5✔
961
                    let callable = callableContainer.callable;
10✔
962
                    const related = [];
10✔
963
                    for (const ownCallable of ownCallables) {
10✔
964
                        const thatNameRange = ownCallable.callable.nameRange;
20✔
965
                        if (ownCallable.callable.nameRange !== callable.nameRange) {
20✔
966
                            related.push({
10✔
967
                                message: `Function declared here`,
968
                                location: util.createLocationFromRange(
969
                                    URI.file(ownCallable.callable.file?.srcPath).toString(),
30!
970
                                    thatNameRange
971
                                )
972
                            });
973
                        }
974
                    }
975

976
                    this.addMultiScopeDiagnostic({
10✔
977
                        ...DiagnosticMessages.duplicateFunctionImplementation(callable.name),
978
                        range: callable.nameRange,
979
                        file: callable.file,
980
                        relatedInformation: related
981
                    }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
982
                }
983
            }
984
        }
985
    }
986

987
    /**
988
     * Verify that all of the scripts imported by each file in this scope actually exist, and have the correct case
989
     */
990
    private validateScriptImportPaths() {
991
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Imports });
1,541✔
992

993
        let scriptImports = this.event.scope.getOwnScriptImports();
1,541✔
994
        //verify every script import
995
        for (let scriptImport of scriptImports) {
1,541✔
996
            let referencedFile = this.event.scope.getFileByRelativePath(scriptImport.destPath);
492✔
997
            //if we can't find the file
998
            if (!referencedFile) {
492✔
999
                //skip the default bslib file, it will exist at transpile time but should not show up in the program during validation cycle
1000
                if (scriptImport.destPath === this.event.program.bslibPkgPath) {
15✔
1001
                    continue;
2✔
1002
                }
1003
                let dInfo: DiagnosticInfo;
1004
                if (scriptImport.text.trim().length === 0) {
13✔
1005
                    dInfo = DiagnosticMessages.scriptSrcCannotBeEmpty();
1✔
1006
                } else {
1007
                    dInfo = DiagnosticMessages.referencedFileDoesNotExist();
12✔
1008
                }
1009

1010
                this.addMultiScopeDiagnostic({
13✔
1011
                    ...dInfo,
1012
                    range: scriptImport.filePathRange,
1013
                    file: scriptImport.sourceFile
1014
                }, ScopeValidatorDiagnosticTag.Imports);
1015
                //if the character casing of the script import path does not match that of the actual path
1016
            } else if (scriptImport.destPath !== referencedFile.destPath) {
477✔
1017
                this.addMultiScopeDiagnostic({
2✔
1018
                    ...DiagnosticMessages.scriptImportCaseMismatch(referencedFile.destPath),
1019
                    range: scriptImport.filePathRange,
1020
                    file: scriptImport.sourceFile
1021
                }, ScopeValidatorDiagnosticTag.Imports);
1022
            }
1023
        }
1024
    }
1025

1026
    /**
1027
     * Validate all classes defined in this scope
1028
     */
1029
    private validateClasses() {
1030
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Classes });
1,541✔
1031

1032
        let validator = new BsClassValidator(this.event.scope);
1,541✔
1033
        validator.validate();
1,541✔
1034
        for (const diagnostic of validator.diagnostics) {
1,541✔
1035
            this.addMultiScopeDiagnostic({
29✔
1036
                ...diagnostic
1037
            }, ScopeValidatorDiagnosticTag.Classes);
1038
        }
1039
    }
1040

1041

1042
    /**
1043
     * Find various function collisions
1044
     */
1045
    private diagnosticDetectFunctionCollisions(file: BrsFile) {
1046
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, file: file, tag: ScopeValidatorDiagnosticTag.FunctionCollisions });
1,768✔
1047
        for (let func of file.callables) {
1,768✔
1048
            const funcName = func.getName(ParseMode.BrighterScript);
1,538✔
1049
            const lowerFuncName = funcName?.toLowerCase();
1,538!
1050
            if (lowerFuncName) {
1,538!
1051

1052
                //find function declarations with the same name as a stdlib function
1053
                if (globalCallableMap.has(lowerFuncName)) {
1,538✔
1054
                    this.addMultiScopeDiagnostic({
5✔
1055
                        ...DiagnosticMessages.scopeFunctionShadowedByBuiltInFunction(),
1056
                        range: func.nameRange,
1057
                        file: file
1058

1059
                    });
1060
                }
1061
            }
1062
        }
1063
    }
1064

1065
    public detectShadowedLocalVar(file: BrsFile, varDeclaration: { name: string; type: BscType; nameRange: Range }) {
1066
        const varName = varDeclaration.name;
1,620✔
1067
        const lowerVarName = varName.toLowerCase();
1,620✔
1068
        const callableContainerMap = this.event.scope.getCallableContainerMap();
1,620✔
1069

1070
        const varIsFunction = () => {
1,620✔
1071
            return isCallableType(varDeclaration.type);
11✔
1072
        };
1073

1074
        if (
1,620✔
1075
            //has same name as stdlib
1076
            globalCallableMap.has(lowerVarName)
1077
        ) {
1078
            //local var function with same name as stdlib function
1079
            if (varIsFunction()) {
8✔
1080
                this.addMultiScopeDiagnostic({
1✔
1081
                    ...DiagnosticMessages.localVarFunctionShadowsParentFunction('stdlib'),
1082
                    range: varDeclaration.nameRange,
1083
                    file: file
1084
                });
1085
            }
1086
        } else if (callableContainerMap.has(lowerVarName)) {
1,612✔
1087
            const callable = callableContainerMap.get(lowerVarName);
3✔
1088
            //is same name as a callable
1089
            if (varIsFunction()) {
3✔
1090
                this.addMultiScopeDiagnostic({
1✔
1091
                    ...DiagnosticMessages.localVarFunctionShadowsParentFunction('scope'),
1092
                    range: varDeclaration.nameRange,
1093
                    file: file,
1094
                    relatedInformation: [{
1095
                        message: 'Function declared here',
1096
                        location: util.createLocationFromRange(
1097
                            URI.file(callable[0].callable.file.srcPath).toString(),
1098
                            callable[0].callable.nameRange
1099
                        )
1100
                    }]
1101
                });
1102
            } else {
1103
                this.addMultiScopeDiagnostic({
2✔
1104
                    ...DiagnosticMessages.localVarShadowedByScopedFunction(),
1105
                    range: varDeclaration.nameRange,
1106
                    file: file,
1107
                    relatedInformation: [{
1108
                        message: 'Function declared here',
1109
                        location: util.createLocationFromRange(
1110
                            URI.file(callable[0].callable.file.srcPath).toString(),
1111
                            callable[0].callable.nameRange
1112
                        )
1113
                    }]
1114
                });
1115
            }
1116
            //has the same name as an in-scope class
1117
        } else {
1118
            const classStmtLink = this.event.scope.getClassFileLink(lowerVarName);
1,609✔
1119
            if (classStmtLink) {
1,609✔
1120
                this.addMultiScopeDiagnostic({
3✔
1121
                    ...DiagnosticMessages.localVarSameNameAsClass(classStmtLink?.item?.getName(ParseMode.BrighterScript)),
18!
1122
                    range: varDeclaration.nameRange,
1123
                    file: file,
1124
                    relatedInformation: [{
1125
                        message: 'Class declared here',
1126
                        location: util.createLocationFromRange(
1127
                            URI.file(classStmtLink.file.srcPath).toString(),
1128
                            classStmtLink?.item.tokens.name.location?.range
18!
1129
                        )
1130
                    }]
1131
                });
1132
            }
1133
        }
1134
    }
1135

1136
    private validateXmlInterface(scope: XmlScope) {
1137
        if (!scope.xmlFile.parser.ast?.componentElement?.interfaceElement) {
428!
1138
            return;
397✔
1139
        }
1140
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, file: scope.xmlFile, tag: ScopeValidatorDiagnosticTag.XMLInterface });
31✔
1141

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

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

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

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

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

1263
        // by default return the result of node.getType()
1264
        return type;
7,684✔
1265
    }
1266

1267
    private getParentTypeDescriptor(typeChainResult: TypeChainProcessResult) {
1268
        if (typeChainResult.itemParentTypeKind === BscTypeKind.NamespaceType) {
231✔
1269
            return 'namespace';
117✔
1270
        }
1271
        return 'type';
114✔
1272
    }
1273

1274
    private addDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1275
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
51!
1276
        this.event.program.diagnostics.register(diagnostic, {
51✔
1277
            tags: [diagnosticTag],
1278
            segment: this.currentSegmentBeingValidated
1279
        });
1280
    }
1281

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