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

rokucommunity / brighterscript / #12716

14 Jun 2024 08:20PM UTC coverage: 85.629% (-2.3%) from 87.936%
#12716

push

web-flow
Merge 94311dc0a into 42db50190

10808 of 13500 branches covered (80.06%)

Branch coverage included in aggregate %.

6557 of 7163 new or added lines in 96 files covered. (91.54%)

83 existing lines in 17 files now uncovered.

12270 of 13451 relevant lines covered (91.22%)

26529.43 hits per line

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

88.59
/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,530✔
65

66

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

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

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

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

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

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

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

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

116
        this.event.scope.enumerateOwnFiles((file) => {
1,534✔
117
            if (isBrsFile(file)) {
2,321✔
118
                const thisFileHasChanges = this.event.changedFiles.includes(file);
1,894✔
119

120
                const hasUnvalidatedSegments = file.validationSegmenter.hasUnvalidatedSegments();
1,894✔
121

122
                if (hasChangeInfo && !hasUnvalidatedSegments) {
1,894✔
123
                    return;
397✔
124
                }
125

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

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

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

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

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

224
    private currentSegmentBeingValidated: AstNode;
225

226

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

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

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

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

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

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

331
    }
332

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

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

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

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

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

403
        }
404
    }
405

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

413

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

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

431
            }
432
        }
433
    }
434

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

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

461
        let accessibilityIsOk = this.checkMemberAccessibility(file, dottedSetStmt, typeChainExpectedLHS);
38✔
462

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

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

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

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

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

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

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

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

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

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

547

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

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

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

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

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

584

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

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

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

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

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

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

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

638

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

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

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

672
        const hasValidDeclaration = this.hasValidDeclaration(expression, exprType, typeData?.definingNode);
3,402!
673

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

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

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

738
            }
739
        }
740
        if (isUsedAsType) {
3,402✔
741
            return;
1,086✔
742
        }
743

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

746
        if (!(isCallExpression(expression.parent) && isNewExpression(expression.parent?.parent))) {
2,316!
747
            const classUsedAsVarEntry = this.checkTypeChainForClassUsedAsVar(typeChain, containingNamespaceName);
2,203✔
748
            if (classUsedAsVarEntry) {
2,203!
749

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

759
        const lastTypeInfo = typeChain[typeChain.length - 1];
2,316✔
760
        const parentTypeInfo = typeChain[typeChain.length - 2];
2,316✔
761

762
        this.checkMemberAccessibility(file, expression, typeChain);
2,316✔
763

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

800
    private checkTypeChainForClassUsedAsVar(typeChain: TypeChainEntry[], containingNamespaceName: string) {
801
        const ignoreKinds = [AstNodeKind.TypecastExpression, AstNodeKind.NewExpression];
2,203✔
802
        let lowerNameSoFar = '';
2,203✔
803
        let classUsedAsVar;
804
        let isFirst = true;
2,203✔
805
        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✔
806
            const tce = typeChain[i];
1,193✔
807
            lowerNameSoFar += `${lowerNameSoFar ? '.' : ''}${tce.name.toLowerCase()}`;
1,193✔
808
            if (!isNamespaceType(tce.type)) {
1,193✔
809
                if (isFirst && containingNamespaceName) {
538✔
810
                    lowerNameSoFar = `${containingNamespaceName.toLowerCase()}.${lowerNameSoFar}`;
59✔
811
                }
812
                if (!tce.astNode || ignoreKinds.includes(tce.astNode.kind)) {
538✔
813
                    break;
12✔
814
                } else if (isClassType(tce.type) && lowerNameSoFar.toLowerCase() === tce.type.name.toLowerCase()) {
526!
NEW
815
                    classUsedAsVar = tce.type;
×
816
                }
817
                break;
526✔
818
            }
819
            isFirst = false;
655✔
820
        }
821

822
        return classUsedAsVar;
2,203✔
823
    }
824

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

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

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

875
            }
876
        }
877
        return true;
2,345✔
878
    }
879

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

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

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

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

904
        }
905
    }
906

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

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

916
            let globalCallables = [] as CallableContainer[];
115,172✔
917
            let nonGlobalCallables = [] as CallableContainer[];
115,172✔
918
            let ownCallables = [] as CallableContainer[];
115,172✔
919
            let ancestorNonGlobalCallables = [] as CallableContainer[];
115,172✔
920

921
            for (let container of callableContainers) {
115,172✔
922
                if (container.scope === this.event.program.globalScope) {
121,342✔
923
                    globalCallables.push(container);
119,652✔
924
                } else {
925
                    nonGlobalCallables.push(container);
1,690✔
926
                    if (container.scope === this.event.scope) {
1,690✔
927
                        ownCallables.push(container);
1,660✔
928
                    } else {
929
                        ancestorNonGlobalCallables.push(container);
30✔
930
                    }
931
                }
932
            }
933

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

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

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

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

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

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

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

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

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

1043

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

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

1061
                    });
1062
                }
1063
            }
1064
        }
1065
    }
1066

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

1072
        const varIsFunction = () => {
1,612✔
1073
            return isCallableType(varDeclaration.type);
11✔
1074
        };
1075

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

1138
    private detectVariableNamespaceCollisions(file: BrsFile) {
1139
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, file: file, tag: ScopeValidatorDiagnosticTag.NamespaceCollisions });
1,904✔
1140

1141
        //find all function parameters
1142
        // eslint-disable-next-line @typescript-eslint/dot-notation
1143
        for (let func of file['_cachedLookups'].functionExpressions) {
1,904✔
1144
            for (let param of func.parameters) {
1,693✔
1145
                let lowerParamName = param.tokens.name.text.toLowerCase();
950✔
1146
                let namespace = this.event.scope.getNamespace(lowerParamName, param.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript).toLowerCase());
950✔
1147
                //see if the param matches any starting namespace part
1148
                if (namespace) {
950✔
1149
                    this.addMultiScopeDiagnostic({
4✔
1150
                        file: file,
1151
                        ...DiagnosticMessages.parameterMayNotHaveSameNameAsNamespace(param.tokens.name.text),
1152
                        range: param.tokens.name.location?.range,
12!
1153
                        relatedInformation: [{
1154
                            message: 'Namespace declared here',
1155
                            location: util.createLocationFromRange(
1156
                                URI.file(namespace.file.srcPath).toString(),
1157
                                namespace.nameRange
1158
                            )
1159
                        }]
1160
                    }, ScopeValidatorDiagnosticTag.NamespaceCollisions);
1161
                }
1162
            }
1163
        }
1164

1165
        // eslint-disable-next-line @typescript-eslint/dot-notation
1166
        for (let assignment of file['_cachedLookups'].assignmentStatements) {
1,904✔
1167
            let lowerAssignmentName = assignment.tokens.name.text.toLowerCase();
587✔
1168
            let namespace = this.event.scope.getNamespace(lowerAssignmentName, assignment.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript).toLowerCase());
587✔
1169
            //see if the param matches any starting namespace part
1170
            if (namespace) {
587✔
1171
                this.addMultiScopeDiagnostic({
4✔
1172
                    file: file,
1173
                    ...DiagnosticMessages.variableMayNotHaveSameNameAsNamespace(assignment.tokens.name.text),
1174
                    range: assignment.tokens.name.location?.range,
12!
1175
                    relatedInformation: [{
1176
                        message: 'Namespace declared here',
1177
                        location: util.createLocationFromRange(
1178
                            URI.file(namespace.file.srcPath).toString(),
1179
                            namespace.nameRange
1180
                        )
1181
                    }]
1182
                }, ScopeValidatorDiagnosticTag.NamespaceCollisions);
1183
            }
1184
        }
1185
    }
1186

1187
    private validateXmlInterface(scope: XmlScope) {
1188
        if (!scope.xmlFile.parser.ast?.componentElement?.interfaceElement) {
427!
1189
            return;
397✔
1190
        }
1191
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, file: scope.xmlFile, tag: ScopeValidatorDiagnosticTag.XMLInterface });
30✔
1192

1193
        const iface = scope.xmlFile.parser.ast.componentElement.interfaceElement;
30✔
1194
        const callableContainerMap = scope.getCallableContainerMap();
30✔
1195
        //validate functions
1196
        for (const func of iface.functions) {
30✔
1197
            const name = func.name;
31✔
1198
            if (!name) {
31✔
1199
                this.addDiagnostic({
3✔
1200
                    ...DiagnosticMessages.xmlTagMissingAttribute(func.tokens.startTagName.text, 'name'),
1201
                    range: func.tokens.startTagName.location?.range,
9!
1202
                    file: scope.xmlFile
1203
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1204
            } else if (!callableContainerMap.has(name.toLowerCase())) {
28✔
1205
                this.addDiagnostic({
2✔
1206
                    ...DiagnosticMessages.xmlFunctionNotFound(name),
1207
                    range: func.getAttribute('name')?.tokens.value.location?.range,
12!
1208
                    file: scope.xmlFile
1209
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1210
            }
1211
        }
1212
        //validate fields
1213
        for (const field of iface.fields) {
30✔
1214
            const { id, type, onChange } = field;
28✔
1215
            if (!id) {
28✔
1216
                this.addDiagnostic({
3✔
1217
                    ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'id'),
1218
                    range: field.tokens.startTagName.location?.range,
9!
1219
                    file: scope.xmlFile
1220
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1221
            }
1222
            if (!type) {
28✔
1223
                if (!field.alias) {
3✔
1224
                    this.addDiagnostic({
2✔
1225
                        ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'type'),
1226
                        range: field.tokens.startTagName.location?.range,
6!
1227
                        file: scope.xmlFile
1228
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1229
                }
1230
            } else if (!SGFieldTypes.includes(type.toLowerCase())) {
25✔
1231
                this.addDiagnostic({
1✔
1232
                    ...DiagnosticMessages.xmlInvalidFieldType(type),
1233
                    range: field.getAttribute('type')?.tokens.value.location?.range,
6!
1234
                    file: scope.xmlFile
1235
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1236
            }
1237
            if (onChange) {
28✔
1238
                if (!callableContainerMap.has(onChange.toLowerCase())) {
1!
1239
                    this.addDiagnostic({
1✔
1240
                        ...DiagnosticMessages.xmlFunctionNotFound(onChange),
1241
                        range: field.getAttribute('onchange')?.tokens.value.location?.range,
6!
1242
                        file: scope.xmlFile
1243
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1244
                }
1245
            }
1246
        }
1247
    }
1248

1249
    /**
1250
     * Detect when a child has imported a script that an ancestor also imported
1251
     */
1252
    private diagnosticDetectDuplicateAncestorScriptImports(scope: XmlScope) {
1253
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, file: scope.xmlFile, tag: ScopeValidatorDiagnosticTag.XMLImports });
427✔
1254
        if (scope.xmlFile.parentComponent) {
427✔
1255
            //build a lookup of pkg paths -> FileReference so we can more easily look up collisions
1256
            let parentScriptImports = scope.xmlFile.getAncestorScriptTagImports();
34✔
1257
            let lookup = {} as Record<string, FileReference>;
34✔
1258
            for (let parentScriptImport of parentScriptImports) {
34✔
1259
                //keep the first occurance of a pkgPath. Parent imports are first in the array
1260
                if (!lookup[parentScriptImport.destPath]) {
30!
1261
                    lookup[parentScriptImport.destPath] = parentScriptImport;
30✔
1262
                }
1263
            }
1264

1265
            //add warning for every script tag that this file shares with an ancestor
1266
            for (let scriptImport of scope.xmlFile.scriptTagImports) {
34✔
1267
                let ancestorScriptImport = lookup[scriptImport.destPath];
30✔
1268
                if (ancestorScriptImport) {
30✔
1269
                    let ancestorComponent = ancestorScriptImport.sourceFile as XmlFile;
21✔
1270
                    let ancestorComponentName = ancestorComponent.componentName?.text ?? ancestorComponent.destPath;
21!
1271
                    this.addDiagnostic({
21✔
1272
                        file: scope.xmlFile,
1273
                        range: scriptImport.filePathRange,
1274
                        ...DiagnosticMessages.unnecessaryScriptImportInChildFromParent(ancestorComponentName)
1275
                    }, ScopeValidatorDiagnosticTag.XMLImports);
1276
                }
1277
            }
1278
        }
1279
    }
1280

1281
    /**
1282
     * Wraps the AstNode.getType() method, so that we can do extra processing on the result based on the current file
1283
     * In particular, since BrightScript does not support Unions, and there's no way to cast them to something else
1284
     * if the result of .getType() is a union, and we're in a .brs (brightScript) file, treat the result as Dynamic
1285
     *
1286
     * In most cases, this returns the result of node.getType()
1287
     *
1288
     * @param file the current file being processed
1289
     * @param node the node to get the type of
1290
     * @param getTypeOpts any options to pass to node.getType()
1291
     * @returns the processed result type
1292
     */
1293
    private getNodeTypeWrapper(file: BrsFile, node: AstNode, getTypeOpts: GetTypeOptions) {
1294
        const type = node?.getType(getTypeOpts);
7,673✔
1295

1296
        if (file.parseMode === ParseMode.BrightScript) {
7,673✔
1297
            // this is a brightscript file
1298
            const typeChain = getTypeOpts.typeChain;
883✔
1299
            if (typeChain) {
883✔
1300
                const hasUnion = typeChain.reduce((hasUnion, tce) => {
311✔
1301
                    return hasUnion || isUnionType(tce.type);
355✔
1302
                }, false);
1303
                if (hasUnion) {
311✔
1304
                    // there was a union somewhere in the typechain
1305
                    return DynamicType.instance;
6✔
1306
                }
1307
            }
1308
            if (isUnionType(type)) {
877✔
1309
                //this is a union
1310
                return DynamicType.instance;
4✔
1311
            }
1312
        }
1313

1314
        // by default return the result of node.getType()
1315
        return type;
7,663✔
1316
    }
1317

1318
    private getParentTypeDescriptor(typeChainResult: TypeChainProcessResult) {
1319
        if (typeChainResult.itemParentTypeKind === BscTypeKind.NamespaceType) {
231✔
1320
            return 'namespace';
117✔
1321
        }
1322
        return 'type';
114✔
1323
    }
1324

1325
    private addDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1326
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
49!
1327
        this.event.program.diagnostics.register(diagnostic, {
49✔
1328
            tags: [diagnosticTag],
1329
            segment: this.currentSegmentBeingValidated
1330
        });
1331
    }
1332

1333
    /**
1334
     * Add a diagnostic (to the first scope) that will have `relatedInformation` for each affected scope
1335
     */
1336
    private addMultiScopeDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1337
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
450✔
1338
        this.event.program.diagnostics.register(diagnostic, {
450✔
1339
            tags: [diagnosticTag],
1340
            segment: this.currentSegmentBeingValidated,
1341
            scope: this.event.scope
1342
        });
1343
    }
1344
}
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