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

rokucommunity / brighterscript / #13605

13 Jan 2025 08:51PM UTC coverage: 86.913% (+0.01%) from 86.902%
#13605

push

web-flow
Merge 8c5f8741e into 9d6ef67ba

11976 of 14547 branches covered (82.33%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

103 existing lines in 10 files now uncovered.

12982 of 14169 relevant lines covered (91.62%)

31777.91 hits per line

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

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

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

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

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

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

67
    private metrics = new Map<string, number>();
1,830✔
68

69

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

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

97
    public reset() {
98
        this.event = undefined;
1,366✔
99
    }
100

101
    private walkFiles() {
102
        const hasChangeInfo = this.event.changedFiles && this.event.changedSymbols;
1,676✔
103

104
        //do many per-file checks for every file in this (and parent) scopes
105
        this.event.scope.enumerateBrsFiles((file) => {
1,676✔
106
            if (!isBrsFile(file)) {
2,058!
UNCOV
107
                return;
×
108
            }
109

110
            const thisFileHasChanges = this.event.changedFiles.includes(file);
2,058✔
111

112
            if (thisFileHasChanges || this.doesFileProvideChangedSymbol(file, this.event.changedSymbols)) {
2,058✔
113
                this.diagnosticDetectFunctionCollisions(file);
1,915✔
114
            }
115
        });
116

117
        this.event.scope.enumerateOwnFiles((file) => {
1,676✔
118
            if (isBrsFile(file)) {
2,476✔
119

120
                const fileUri = util.pathToUri(file.srcPath);
2,048✔
121
                const thisFileHasChanges = this.event.changedFiles.includes(file);
2,048✔
122

123
                const hasUnvalidatedSegments = file.validationSegmenter.hasUnvalidatedSegments();
2,048✔
124

125
                if (hasChangeInfo && !hasUnvalidatedSegments) {
2,048✔
126
                    return;
401✔
127
                }
128

129
                const validationVisitor = createVisitor({
1,647✔
130
                    VariableExpression: (varExpr) => {
131
                        this.validateVariableAndDottedGetExpressions(file, varExpr);
3,605✔
132
                    },
133
                    DottedGetExpression: (dottedGet) => {
134
                        this.validateVariableAndDottedGetExpressions(file, dottedGet);
1,477✔
135
                    },
136
                    CallExpression: (functionCall) => {
137
                        this.validateFunctionCall(file, functionCall);
912✔
138
                        this.validateCreateObjectCall(file, functionCall);
912✔
139
                    },
140
                    ReturnStatement: (returnStatement) => {
141
                        this.validateReturnStatement(file, returnStatement);
333✔
142
                    },
143
                    DottedSetStatement: (dottedSetStmt) => {
144
                        this.validateDottedSetStatement(file, dottedSetStmt);
90✔
145
                    },
146
                    BinaryExpression: (binaryExpr) => {
147
                        this.validateBinaryExpression(file, binaryExpr);
256✔
148
                    },
149
                    UnaryExpression: (unaryExpr) => {
150
                        this.validateUnaryExpression(file, unaryExpr);
33✔
151
                    },
152
                    AssignmentStatement: (assignStmt) => {
153
                        this.validateAssignmentStatement(file, assignStmt);
650✔
154
                        // Note: this also includes For statements
155
                        this.detectShadowedLocalVar(file, {
650✔
156
                            expr: assignStmt,
157
                            name: assignStmt.tokens.name.text,
158
                            type: this.getNodeTypeWrapper(file, assignStmt, { flags: SymbolTypeFlag.runtime }),
159
                            nameRange: assignStmt.tokens.name.location?.range
1,950✔
160
                        });
161
                    },
162
                    AugmentedAssignmentStatement: (binaryExpr) => {
163
                        this.validateBinaryExpression(file, binaryExpr);
56✔
164
                    },
165
                    IncrementStatement: (stmt) => {
166
                        this.validateIncrementStatement(file, stmt);
9✔
167
                    },
168
                    NewExpression: (newExpr) => {
169
                        this.validateNewExpression(file, newExpr);
121✔
170
                    },
171
                    ForEachStatement: (forEachStmt) => {
172
                        this.detectShadowedLocalVar(file, {
24✔
173
                            expr: forEachStmt,
174
                            name: forEachStmt.tokens.item.text,
175
                            type: this.getNodeTypeWrapper(file, forEachStmt, { flags: SymbolTypeFlag.runtime }),
176
                            nameRange: forEachStmt.tokens.item.location?.range
72✔
177
                        });
178
                    },
179
                    FunctionParameterExpression: (funcParam) => {
180
                        this.detectShadowedLocalVar(file, {
1,071✔
181
                            expr: funcParam,
182
                            name: funcParam.tokens.name.text,
183
                            type: this.getNodeTypeWrapper(file, funcParam, { flags: SymbolTypeFlag.runtime }),
184
                            nameRange: funcParam.tokens.name.location?.range
3,213✔
185
                        });
186
                    },
187
                    FunctionExpression: (func) => {
188
                        this.validateFunctionExpressionForReturn(func);
1,994✔
189
                    },
190
                    AstNode: (node) => {
191
                        //check for doc comments
192
                        if (!node.leadingTrivia || node.leadingTrivia.filter(triviaToken => triviaToken.kind === TokenKind.Comment).length === 0) {
24,297✔
193
                            return;
18,544✔
194
                        }
195
                        this.validateDocComments(node);
242✔
196
                    }
197
                });
198
                // validate only what's needed in the file
199

200
                const segmentsToWalkForValidation = thisFileHasChanges
1,647✔
201
                    ? file.validationSegmenter.getAllUnvalidatedSegments()
1,647✔
202
                    : file.validationSegmenter.getSegmentsWithChangedSymbols(this.event.changedSymbols);
203

204
                let segmentsValidated = 0;
1,647✔
205
                for (const segment of segmentsToWalkForValidation) {
1,647✔
206
                    if (!file.validationSegmenter.checkIfSegmentNeedsRevalidation(segment, this.event.changedSymbols)) {
3,171!
UNCOV
207
                        continue;
×
208
                    }
209
                    this.currentSegmentBeingValidated = segment;
3,171✔
210
                    this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, segment: segment, tag: ScopeValidatorDiagnosticTag.Segment });
3,171✔
211
                    segmentsValidated++;
3,171✔
212
                    segment.walk(validationVisitor, {
3,171✔
213
                        walkMode: InsideSegmentWalkMode
214
                    });
215
                    file.markSegmentAsValidated(segment);
3,171✔
216
                    this.currentSegmentBeingValidated = null;
3,171✔
217
                }
218
                this.metrics.set(file.pkgPath, segmentsValidated);
1,647✔
219
            }
220
        });
221
    }
222

223
    private doesFileProvideChangedSymbol(file: BrsFile, changedSymbols: Map<SymbolTypeFlag, Set<string>>) {
224
        if (!changedSymbols) {
143!
UNCOV
225
            return true;
×
226
        }
227
        for (const flag of [SymbolTypeFlag.runtime, SymbolTypeFlag.typetime]) {
143✔
228
            const providedSymbolKeysFlag = file.providedSymbols.symbolMap.get(flag).keys();
286✔
229
            const changedSymbolSetForFlag = changedSymbols.get(flag);
286✔
230

231
            for (let providedKey of providedSymbolKeysFlag) {
286✔
232
                if (changedSymbolSetForFlag.has(providedKey)) {
270!
UNCOV
233
                    return true;
×
234
                }
235
            }
236
        }
237
        return false;
143✔
238
    }
239

240
    private currentSegmentBeingValidated: AstNode;
241

242

243
    private isTypeKnown(exprType: BscType) {
244
        let isKnownType = exprType?.isResolvable();
3,708✔
245
        return isKnownType;
3,708✔
246
    }
247

248
    /**
249
     * If this is the lhs of an assignment, we don't need to flag it as unresolved
250
     */
251
    private hasValidDeclaration(expression: Expression, exprType: BscType, definingNode?: AstNode) {
252
        if (!isVariableExpression(expression)) {
3,708✔
253
            return false;
1,059✔
254
        }
255
        let assignmentAncestor: AssignmentStatement;
256
        if (isAssignmentStatement(definingNode) && definingNode.tokens.equals.kind === TokenKind.Equal) {
2,649✔
257
            // this symbol was defined in a "normal" assignment (eg. not a compound assignment)
258
            assignmentAncestor = definingNode;
330✔
259
            return assignmentAncestor?.tokens.name?.text.toLowerCase() === expression?.tokens.name?.text.toLowerCase();
330!
260
        } else if (isFunctionParameterExpression(definingNode)) {
2,319✔
261
            // this symbol was defined in a function param
262
            return true;
475✔
263
        } else {
264
            assignmentAncestor = expression?.findAncestor(isAssignmentStatement);
1,844!
265
        }
266
        return assignmentAncestor?.tokens.name === expression?.tokens.name && isUnionType(exprType);
1,844!
267
    }
268

269
    /**
270
     * Validate every function call to `CreateObject`.
271
     * Ideally we would create better type checking/handling for this, but in the mean time, we know exactly
272
     * what these calls are supposed to look like, and this is a very common thing for brs devs to do, so just
273
     * do this manually for now.
274
     */
275
    protected validateCreateObjectCall(file: BrsFile, call: CallExpression) {
276

277
        //skip non CreateObject function calls
278
        const callName = util.getAllDottedGetPartsAsString(call.callee)?.toLowerCase();
912✔
279
        if (callName !== 'createobject' || !isLiteralExpression(call?.args[0])) {
912!
280
            return;
848✔
281
        }
282
        const firstParamToken = (call?.args[0] as LiteralExpression)?.tokens?.value;
64!
283
        const firstParamStringValue = firstParamToken?.text?.replace(/"/g, '');
64!
284
        if (!firstParamStringValue) {
64!
UNCOV
285
            return;
×
286
        }
287
        const firstParamStringValueLower = firstParamStringValue.toLowerCase();
64✔
288

289
        //if this is a `createObject('roSGNode'` call, only support known sg node types
290
        if (firstParamStringValueLower === 'rosgnode' && isLiteralExpression(call?.args[1])) {
64!
291
            const componentName: Token = call?.args[1]?.tokens.value;
26!
292
            //don't validate any components with a colon in their name (probably component libraries, but regular components can have them too).
293
            if (!componentName || componentName?.text?.includes(':')) {
26!
294
                return;
3✔
295
            }
296
            //add diagnostic for unknown components
297
            const unquotedComponentName = componentName?.text?.replace(/"/g, '');
23!
298
            if (unquotedComponentName && !platformNodeNames.has(unquotedComponentName.toLowerCase()) && !this.event.program.getComponent(unquotedComponentName)) {
23✔
299
                this.addDiagnostic({
4✔
300
                    ...DiagnosticMessages.unknownRoSGNode(unquotedComponentName),
301
                    location: componentName.location
302
                });
303
            } else if (call?.args.length !== 2) {
19!
304
                // roSgNode should only ever have 2 args in `createObject`
305
                this.addDiagnostic({
1✔
306
                    ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, [2], call?.args.length),
3!
307
                    location: call.location
308
                });
309
            }
310
        } else if (!platformComponentNames.has(firstParamStringValueLower)) {
38✔
311
            this.addDiagnostic({
7✔
312
                ...DiagnosticMessages.unknownBrightScriptComponent(firstParamStringValue),
313
                location: firstParamToken.location
314
            });
315
        } else {
316
            // This is valid brightscript component
317
            // Test for invalid arg counts
318
            const brightScriptComponent: BRSComponentData = components[firstParamStringValueLower];
31✔
319
            // Valid arg counts for createObject are 1+ number of args for constructor
320
            let validArgCounts = brightScriptComponent?.constructors.map(cnstr => cnstr.params.length + 1);
34!
321
            if (validArgCounts.length === 0) {
31✔
322
                // no constructors for this component, so createObject only takes 1 arg
323
                validArgCounts = [1];
2✔
324
            }
325
            if (!validArgCounts.includes(call?.args.length)) {
31!
326
                // Incorrect number of arguments included in `createObject()`
327
                this.addDiagnostic({
4✔
328
                    ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, validArgCounts, call?.args.length),
12!
329
                    location: call.location
330
                });
331
            }
332

333
            // Test for deprecation
334
            if (brightScriptComponent?.isDeprecated) {
31!
UNCOV
335
                this.addDiagnostic({
×
336
                    ...DiagnosticMessages.itemIsDeprecated(firstParamStringValue, brightScriptComponent.deprecatedDescription),
337
                    location: call.location
338
                });
339
            }
340
        }
341

342
    }
343

344
    /**
345
     * Detect calls to functions with the incorrect number of parameters, or wrong types of arguments
346
     */
347
    private validateFunctionCall(file: BrsFile, expression: CallExpression) {
348
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: {} };
912✔
349
        let funcType = this.getNodeTypeWrapper(file, expression?.callee, getTypeOptions);
912!
350
        if (funcType?.isResolvable() && isClassType(funcType)) {
912✔
351
            // We're calling a class - get the constructor
352
            funcType = funcType.getMemberType('new', getTypeOptions);
130✔
353
        }
354
        if (funcType?.isResolvable() && isTypedFunctionType(funcType)) {
912✔
355
            //funcType.setName(expression.callee. .name);
356

357
            //get min/max parameter count for callable
358
            let minParams = 0;
650✔
359
            let maxParams = 0;
650✔
360
            for (let param of funcType.params) {
650✔
361
                maxParams++;
890✔
362
                //optional parameters must come last, so we can assume that minParams won't increase once we hit
363
                //the first isOptional
364
                if (param.isOptional !== true) {
890✔
365
                    minParams++;
482✔
366
                }
367
            }
368
            if (funcType.isVariadic) {
650✔
369
                // function accepts variable number of arguments
370
                maxParams = CallExpression.MaximumArguments;
3✔
371
            }
372
            let expCallArgCount = expression.args.length;
650✔
373
            if (expCallArgCount > maxParams || expCallArgCount < minParams) {
650✔
374
                let minMaxParamsText = minParams === maxParams ? maxParams : `${minParams}-${maxParams}`;
32✔
375
                this.addMultiScopeDiagnostic({
32✔
376
                    ...DiagnosticMessages.mismatchArgumentCount(minMaxParamsText, expCallArgCount),
377
                    location: expression.callee.location
378
                });
379
            }
380
            let paramIndex = 0;
650✔
381
            for (let arg of expression.args) {
650✔
382
                const data = {} as ExtraSymbolData;
557✔
383
                let argType = this.getNodeTypeWrapper(file, arg, { flags: SymbolTypeFlag.runtime, data: data });
557✔
384

385
                let paramType = funcType.params[paramIndex]?.type;
557✔
386
                if (!paramType) {
557✔
387
                    // unable to find a paramType -- maybe there are more args than params
388
                    break;
16✔
389
                }
390

391
                if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) {
541✔
392
                    argType = data.definingNode.getConstructorType();
2✔
393
                }
394

395
                const compatibilityData: TypeCompatibilityData = {};
541✔
396
                const isAllowedArgConversion = this.checkAllowedArgConversions(paramType, argType);
541✔
397
                if (!isAllowedArgConversion && !paramType?.isTypeCompatible(argType, compatibilityData)) {
541!
398
                    this.addMultiScopeDiagnostic({
35✔
399
                        ...DiagnosticMessages.argumentTypeMismatch(argType.toString(), paramType.toString(), compatibilityData),
400
                        location: arg.location
401
                    });
402
                }
403
                paramIndex++;
541✔
404
            }
405

406
        }
407
    }
408

409
    private checkAllowedArgConversions(paramType: BscType, argType: BscType): boolean {
410
        if (isNumberType(argType) && isBooleanType(paramType)) {
541✔
411
            return true;
8✔
412
        }
413
        return false;
533✔
414
    }
415

416

417
    /**
418
     * Detect return statements with incompatible types vs. declared return type
419
     */
420
    private validateReturnStatement(file: BrsFile, returnStmt: ReturnStatement) {
421
        const data: ExtraSymbolData = {};
334✔
422
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: data };
334✔
423
        let funcType = returnStmt.findAncestor(isFunctionExpression)?.getType({ flags: SymbolTypeFlag.typetime });
334✔
424
        if (isTypedFunctionType(funcType)) {
334✔
425
            let actualReturnType = returnStmt?.value
333!
426
                ? this.getNodeTypeWrapper(file, returnStmt?.value, getTypeOptions)
1,293!
427
                : VoidType.instance;
428
            const compatibilityData: TypeCompatibilityData = {};
333✔
429

430
            // `return` statement by itself in non-built-in function will actually result in `invalid`
431
            const valueReturnType = isVoidType(actualReturnType) ? InvalidType.instance : actualReturnType;
333✔
432

433
            if (funcType.returnType.isResolvable()) {
333✔
434
                if (!returnStmt?.value && isVoidType(funcType.returnType)) {
329!
435
                    // allow empty return when function is return `as void`
436
                    // eslint-disable-next-line no-useless-return
437
                    return;
9✔
438
                } else if (!funcType.returnType.isTypeCompatible(valueReturnType, compatibilityData)) {
320✔
439
                    this.addMultiScopeDiagnostic({
14✔
440
                        ...DiagnosticMessages.returnTypeMismatch(actualReturnType.toString(), funcType.returnType.toString(), compatibilityData),
441
                        location: returnStmt.value?.location ?? returnStmt.location
84✔
442
                    });
443
                }
444
            }
445
        }
446
    }
447

448
    /**
449
     * Detect assigned type different from expected member type
450
     */
451
    private validateDottedSetStatement(file: BrsFile, dottedSetStmt: DottedSetStatement) {
452
        const typeChainExpectedLHS = [] as TypeChainEntry[];
90✔
453
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
90✔
454

455
        const expectedLHSType = this.getNodeTypeWrapper(file, dottedSetStmt, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
90✔
456
        const actualRHSType = this.getNodeTypeWrapper(file, dottedSetStmt?.value, getTypeOpts);
90!
457
        const compatibilityData: TypeCompatibilityData = {};
90✔
458
        const typeChainScan = util.processTypeChain(typeChainExpectedLHS);
90✔
459
        // check if anything in typeChain is an AA - if so, just allow it
460
        if (typeChainExpectedLHS.find(typeChainItem => isAssociativeArrayType(typeChainItem.type))) {
147✔
461
            // something in the chain is an AA
462
            // treat members as dynamic - they could have been set without the type system's knowledge
463
            return;
39✔
464
        }
465
        if (!expectedLHSType || !expectedLHSType?.isResolvable()) {
51!
466
            this.addMultiScopeDiagnostic({
5✔
467
                ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
468
                location: typeChainScan?.location
15!
469
            });
470
            return;
5✔
471
        }
472

473
        let accessibilityIsOk = this.checkMemberAccessibility(file, dottedSetStmt, typeChainExpectedLHS);
46✔
474

475
        //Most Component fields can be set with strings
476
        //TODO: be more precise about which fields can actually accept strings
477
        //TODO: if RHS is a string literal, we can do more validation to make sure it's the correct type
478
        if (isComponentType(dottedSetStmt.obj?.getType({ flags: SymbolTypeFlag.runtime }))) {
46!
479
            if (isStringType(actualRHSType)) {
14✔
480
                return;
5✔
481
            }
482
        }
483

484
        if (accessibilityIsOk && !expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
41!
485
            this.addMultiScopeDiagnostic({
8✔
486
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
487
                location: dottedSetStmt.location
488
            });
489
        }
490
    }
491

492
    /**
493
     * Detect when declared type does not match rhs type
494
     */
495
    private validateAssignmentStatement(file: BrsFile, assignStmt: AssignmentStatement) {
496
        if (!assignStmt?.typeExpression) {
650!
497
            // nothing to check
498
            return;
643✔
499
        }
500

501
        const typeChainExpectedLHS = [];
7✔
502
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
7✔
503
        const expectedLHSType = this.getNodeTypeWrapper(file, assignStmt.typeExpression, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
7✔
504
        const actualRHSType = this.getNodeTypeWrapper(file, assignStmt.value, getTypeOpts);
7✔
505
        const compatibilityData: TypeCompatibilityData = {};
7✔
506
        if (!expectedLHSType || !expectedLHSType.isResolvable()) {
7✔
507
            // LHS is not resolvable... handled elsewhere
508
        } else if (!expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
6!
509
            this.addMultiScopeDiagnostic({
1✔
510
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
511
                location: assignStmt.location
512
            });
513
        }
514
    }
515

516
    /**
517
     * Detect invalid use of a binary operator
518
     */
519
    private validateBinaryExpression(file: BrsFile, binaryExpr: BinaryExpression | AugmentedAssignmentStatement) {
520
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
312✔
521

522
        if (util.isInTypeExpression(binaryExpr)) {
312✔
523
            return;
13✔
524
        }
525

526
        let leftType = isBinaryExpression(binaryExpr)
299✔
527
            ? this.getNodeTypeWrapper(file, binaryExpr.left, getTypeOpts)
299✔
528
            : this.getNodeTypeWrapper(file, binaryExpr.item, getTypeOpts);
529
        let rightType = isBinaryExpression(binaryExpr)
299✔
530
            ? this.getNodeTypeWrapper(file, binaryExpr.right, getTypeOpts)
299✔
531
            : this.getNodeTypeWrapper(file, binaryExpr.value, getTypeOpts);
532

533
        if (!leftType.isResolvable() || !rightType.isResolvable()) {
299✔
534
            // Can not find the type. error handled elsewhere
535
            return;
13✔
536
        }
537

538
        let leftTypeToTest = leftType;
286✔
539
        let rightTypeToTest = rightType;
286✔
540

541
        if (isEnumMemberType(leftType) || isEnumType(leftType)) {
286✔
542
            leftTypeToTest = leftType.underlyingType;
11✔
543
        }
544
        if (isEnumMemberType(rightType) || isEnumType(rightType)) {
286✔
545
            rightTypeToTest = rightType.underlyingType;
10✔
546
        }
547

548
        if (isUnionType(leftType) || isUnionType(rightType)) {
286✔
549
            // TODO: it is possible to validate based on innerTypes, but more complicated
550
            // Because you need to verify each combination of types
551
            return;
2✔
552
        }
553
        const opResult = util.binaryOperatorResultType(leftTypeToTest, binaryExpr.tokens.operator, rightTypeToTest);
284✔
554

555
        if (!opResult) {
284✔
556
            // if the result was dynamic or void, that means there wasn't a valid operation
557
            this.addMultiScopeDiagnostic({
9✔
558
                ...DiagnosticMessages.operatorTypeMismatch(binaryExpr.tokens.operator.text, leftType.toString(), rightType.toString()),
559
                location: binaryExpr.location
560
            });
561
        }
562
    }
563

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

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

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

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

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

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

605
    private validateIncrementStatement(file: BrsFile, incStmt: IncrementStatement) {
606
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
9✔
607

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

610
        if (!rightType.isResolvable()) {
9!
611
            // Can not find the type. error handled elsewhere
UNCOV
612
            return;
×
613
        }
614

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

631

632
    validateVariableAndDottedGetExpressions(file: BrsFile, expression: VariableExpression | DottedGetExpression) {
633
        if (isDottedGetExpression(expression.parent)) {
5,082✔
634
            // We validate dottedGetExpressions at the top-most level
635
            return;
1,371✔
636
        }
637
        if (isVariableExpression(expression)) {
3,711✔
638
            if (isAssignmentStatement(expression.parent) && expression.parent.tokens.name === expression.tokens.name) {
2,652!
639
                // Don't validate LHS of assignments
UNCOV
640
                return;
×
641
            } else if (isNamespaceStatement(expression.parent)) {
2,652✔
642
                return;
3✔
643
            }
644
        }
645

646
        let symbolType = SymbolTypeFlag.runtime;
3,708✔
647
        let oppositeSymbolType = SymbolTypeFlag.typetime;
3,708✔
648
        const isUsedAsType = util.isInTypeExpression(expression);
3,708✔
649
        if (isUsedAsType) {
3,708✔
650
            // This is used in a TypeExpression - only look up types from SymbolTable
651
            symbolType = SymbolTypeFlag.typetime;
1,149✔
652
            oppositeSymbolType = SymbolTypeFlag.runtime;
1,149✔
653
        }
654

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

665
        const hasValidDeclaration = this.hasValidDeclaration(expression, exprType, typeData?.definingNode);
3,708!
666

667
        //include a hint diagnostic if this type is marked as deprecated
668
        if (typeData.flags & SymbolTypeFlag.deprecated) { // eslint-disable-line no-bitwise
3,708✔
669
            this.addMultiScopeDiagnostic({
2✔
670
                ...DiagnosticMessages.itemIsDeprecated(),
671
                location: expression.tokens.name.location,
672
                tags: [DiagnosticTag.Deprecated]
673
            });
674
        }
675

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

710
            } else if (!typeData?.isFromDocComment) {
229!
711
                // only show "cannot find... " errors if the type is not defined from a doc comment
712
                const typeChainScan = util.processTypeChain(typeChain);
227✔
713
                if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
227✔
714
                    this.addMultiScopeDiagnostic({
26✔
715
                        ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
716
                        location: typeChainScan?.location
78!
717
                    });
718
                } else {
719
                    this.addMultiScopeDiagnostic({
201✔
720
                        ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
721
                        location: typeChainScan?.location
603!
722
                    });
723
                }
724

725
            }
726
        }
727
        if (isUsedAsType) {
3,708✔
728
            return;
1,149✔
729
        }
730

731
        const containingNamespace = expression.findAncestor<NamespaceStatement>(isNamespaceStatement);
2,559✔
732
        const containingNamespaceName = containingNamespace?.getName(ParseMode.BrighterScript);
2,559✔
733

734
        if (!(isCallExpression(expression.parent) && isNewExpression(expression.parent?.parent))) {
2,559!
735
            const classUsedAsVarEntry = this.checkTypeChainForClassUsedAsVar(typeChain, containingNamespaceName);
2,438✔
736
            const isClassInNamespace = containingNamespace?.getSymbolTable().hasSymbol(typeChain[0].name, SymbolTypeFlag.runtime);
2,438✔
737
            if (classUsedAsVarEntry && !isClassInNamespace) {
2,438!
738

UNCOV
739
                this.addMultiScopeDiagnostic({
×
740
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable(classUsedAsVarEntry.toString()),
741
                    location: expression.location
742
                });
UNCOV
743
                return;
×
744
            }
745
        }
746

747
        const lastTypeInfo = typeChain[typeChain.length - 1];
2,559✔
748
        const parentTypeInfo = typeChain[typeChain.length - 2];
2,559✔
749

750
        this.checkMemberAccessibility(file, expression, typeChain);
2,559✔
751

752
        if (isNamespaceType(exprType) && !isAliasStatement(expression.parent)) {
2,559✔
753
            this.addMultiScopeDiagnostic({
24✔
754
                ...DiagnosticMessages.itemCannotBeUsedAsVariable('namespace'),
755
                location: expression.location
756
            });
757
        } else if (isEnumType(exprType) && !isAliasStatement(expression.parent)) {
2,535✔
758
            const enumStatement = this.event.scope.getEnum(util.getAllDottedGetPartsAsString(expression));
25✔
759
            if (enumStatement) {
25✔
760
                // there's an enum with this name
761
                this.addMultiScopeDiagnostic({
2✔
762
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable('enum'),
763
                    location: expression.location
764
                });
765
            }
766
        } else if (isDynamicType(exprType) && isEnumType(parentTypeInfo?.type) && isDottedGetExpression(expression)) {
2,510✔
767
            const enumFileLink = this.event.scope.getEnumFileLink(util.getAllDottedGetPartsAsString(expression.obj));
9✔
768
            const typeChainScanForItem = util.processTypeChain(typeChain);
9✔
769
            const typeChainScanForParent = util.processTypeChain(typeChain.slice(0, -1));
9✔
770
            if (enumFileLink) {
9✔
771
                this.addMultiScopeDiagnostic({
5✔
772
                    ...DiagnosticMessages.cannotFindName(lastTypeInfo?.name, typeChainScanForItem.fullChainName, typeChainScanForParent.fullNameOfItem, 'enum'),
15!
773
                    location: lastTypeInfo?.location,
15!
774
                    relatedInformation: [{
775
                        message: 'Enum declared here',
776
                        location: util.createLocationFromRange(
777
                            util.pathToUri(enumFileLink?.file.srcPath),
15!
778
                            enumFileLink?.item?.tokens.name.location?.range
45!
779
                        )
780
                    }]
781
                });
782
            }
783
        }
784
    }
785

786
    private checkTypeChainForClassUsedAsVar(typeChain: TypeChainEntry[], containingNamespaceName: string) {
787
        const ignoreKinds = [AstNodeKind.TypecastExpression, AstNodeKind.NewExpression];
2,438✔
788
        let lowerNameSoFar = '';
2,438✔
789
        let classUsedAsVar;
790
        let isFirst = true;
2,438✔
791
        for (let i = 0; i < typeChain.length - 1; i++) { // do not look at final entry - we CAN use the constructor as a variable
2,438✔
792
            const tce = typeChain[i];
1,281✔
793
            lowerNameSoFar += `${lowerNameSoFar ? '.' : ''}${tce.name.toLowerCase()}`;
1,281✔
794
            if (!isNamespaceType(tce.type)) {
1,281✔
795
                if (isFirst && containingNamespaceName) {
611✔
796
                    lowerNameSoFar = `${containingNamespaceName.toLowerCase()}.${lowerNameSoFar}`;
75✔
797
                }
798
                if (!tce.astNode || ignoreKinds.includes(tce.astNode.kind)) {
611✔
799
                    break;
12✔
800
                } else if (isClassType(tce.type) && lowerNameSoFar.toLowerCase() === tce.type.name.toLowerCase()) {
599✔
801
                    classUsedAsVar = tce.type;
1✔
802
                }
803
                break;
599✔
804
            }
805
            isFirst = false;
670✔
806
        }
807

808
        return classUsedAsVar;
2,438✔
809
    }
810

811
    /**
812
     * Adds diagnostics for accibility mismatches
813
     *
814
     * @param file file
815
     * @param expression containing expression
816
     * @param typeChain type chain to check
817
     * @returns true if member accesiibility is okay
818
     */
819
    private checkMemberAccessibility(file: BscFile, expression: Expression, typeChain: TypeChainEntry[]) {
820
        for (let i = 0; i < typeChain.length - 1; i++) {
2,605✔
821
            const parentChainItem = typeChain[i];
1,434✔
822
            const childChainItem = typeChain[i + 1];
1,434✔
823
            if (isClassType(parentChainItem.type)) {
1,434✔
824
                const containingClassStmt = expression.findAncestor<ClassStatement>(isClassStatement);
158✔
825
                const classStmtThatDefinesChildMember = childChainItem.data?.definingNode?.findAncestor<ClassStatement>(isClassStatement);
158!
826
                if (classStmtThatDefinesChildMember) {
158✔
827
                    const definingClassName = classStmtThatDefinesChildMember.getName(ParseMode.BrighterScript);
156✔
828
                    const inMatchingClassStmt = containingClassStmt?.getName(ParseMode.BrighterScript).toLowerCase() === parentChainItem.type.name.toLowerCase();
156✔
829
                    // eslint-disable-next-line no-bitwise
830
                    if (childChainItem.data.flags & SymbolTypeFlag.private) {
156✔
831
                        if (!inMatchingClassStmt || childChainItem.data.memberOfAncestor) {
15✔
832
                            this.addMultiScopeDiagnostic({
4✔
833
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
834
                                location: expression.location
835
                            });
836
                            // there's an error... don't worry about the rest of the chain
837
                            return false;
4✔
838
                        }
839
                    }
840

841
                    // eslint-disable-next-line no-bitwise
842
                    if (childChainItem.data.flags & SymbolTypeFlag.protected) {
152✔
843
                        const containingClassName = containingClassStmt?.getName(ParseMode.BrighterScript);
13✔
844
                        const containingNamespaceName = expression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
13✔
845
                        const ancestorClasses = this.event.scope.getClassHierarchy(containingClassName, containingNamespaceName).map(link => link.item);
13✔
846
                        const isSubClassOfDefiningClass = ancestorClasses.includes(classStmtThatDefinesChildMember);
13✔
847

848
                        if (!isSubClassOfDefiningClass) {
13✔
849
                            this.addMultiScopeDiagnostic({
5✔
850
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
851
                                location: expression.location
852
                            });
853
                            // there's an error... don't worry about the rest of the chain
854
                            return false;
5✔
855
                        }
856
                    }
857
                }
858

859
            }
860
        }
861
        return true;
2,596✔
862
    }
863

864
    /**
865
     * Find all "new" statements in the program,
866
     * and make sure we can find a class with that name
867
     */
868
    private validateNewExpression(file: BrsFile, newExpression: NewExpression) {
869
        const newExprType = this.getNodeTypeWrapper(file, newExpression, { flags: SymbolTypeFlag.typetime });
121✔
870
        if (isClassType(newExprType)) {
121✔
871
            return;
113✔
872
        }
873

874
        let potentialClassName = newExpression.className.getName(ParseMode.BrighterScript);
8✔
875
        const namespaceName = newExpression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
8!
876
        let newableClass = this.event.scope.getClass(potentialClassName, namespaceName);
8✔
877

878
        if (!newableClass) {
8!
879
            //try and find functions with this name.
880
            let fullName = util.getFullyQualifiedClassName(potentialClassName, namespaceName);
8✔
881

882
            this.addMultiScopeDiagnostic({
8✔
883
                ...DiagnosticMessages.expressionIsNotConstructable(fullName),
884
                location: newExpression.className.location
885
            });
886

887
        }
888
    }
889

890
    private validateFunctionExpressionForReturn(func: FunctionExpression) {
891
        const returnType = func?.returnTypeExpression?.getType({ flags: SymbolTypeFlag.typetime });
1,994!
892

893
        if (!returnType || !returnType.isResolvable() || isVoidType(returnType) || isDynamicType(returnType)) {
1,994✔
894
            return;
1,790✔
895
        }
896
        const returns = func.body?.findChild<ReturnStatement>(isReturnStatement, { walkMode: WalkMode.visitAll });
204!
897
        if (!returns && isStringType(returnType)) {
204✔
898
            this.addMultiScopeDiagnostic({
5✔
899
                ...DiagnosticMessages.returnTypeCoercionMismatch(returnType.toString()),
900
                location: func.location
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,676✔
910

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

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

919

920
            for (let container of callableContainers) {
125,863✔
921
                if (container.scope === this.event.program.globalScope) {
132,601✔
922
                    globalCallables.push(container);
130,728✔
923
                } else {
924
                    nonGlobalCallables.push(container);
1,873✔
925
                    if (container.scope === this.event.scope) {
1,873✔
926
                        ownCallables.push(container);
1,843✔
927
                    } else {
928
                        ancestorNonGlobalCallables.push(container);
30✔
929
                    }
930
                }
931
            }
932

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

957
            //add error diagnostics about duplicate functions in the same scope
958
            if (ownCallables.length > 1) {
125,863✔
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
                                    util.pathToUri(ownCallable.callable.file?.srcPath),
30!
970
                                    thatNameRange
971
                                )
972
                            });
973
                        }
974
                    }
975

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

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

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

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

1023
    /**
1024
     * Validate all classes defined in this scope
1025
     */
1026
    private validateClasses() {
1027
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Classes });
1,676✔
1028

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

1038

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

1050
                //find function declarations with the same name as a stdlib function
1051
                if (globalCallableMap.has(lowerFuncName)) {
1,712✔
1052
                    this.addMultiScopeDiagnostic({
5✔
1053
                        ...DiagnosticMessages.scopeFunctionShadowedByBuiltInFunction(),
1054
                        location: util.createLocationFromRange(fileUri, func.nameRange)
1055

1056
                    });
1057
                }
1058
            }
1059
        }
1060
    }
1061

1062
    public detectShadowedLocalVar(file: BrsFile, varDeclaration: { expr: AstNode; name: string; type: BscType; nameRange: Range }) {
1063
        const varName = varDeclaration.name;
1,745✔
1064
        const lowerVarName = varName.toLowerCase();
1,745✔
1065
        const callableContainerMap = this.event.scope.getCallableContainerMap();
1,745✔
1066
        const containingNamespace = varDeclaration.expr?.findAncestor<NamespaceStatement>(isNamespaceStatement);
1,745!
1067
        const localVarIsInNamespace = util.isVariableMemberOfNamespace(varDeclaration.name, varDeclaration.expr, containingNamespace);
1,745✔
1068

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

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

1131
    private validateXmlInterface(scope: XmlScope) {
1132
        if (!scope.xmlFile.parser.ast?.componentElement?.interfaceElement) {
428!
1133
            return;
397✔
1134
        }
1135
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLInterface });
31!
1136

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

1187
    private validateDocComments(node: AstNode) {
1188
        const doc = brsDocParser.parseNode(node);
242✔
1189
        for (const docTag of doc.tags) {
242✔
1190
            const docTypeTag = docTag as BrsDocWithType;
29✔
1191
            if (!docTypeTag.typeExpression || !docTypeTag.location) {
29✔
1192
                continue;
1✔
1193
            }
1194
            const foundType = docTypeTag.typeExpression?.getType({ flags: SymbolTypeFlag.typetime });
28!
1195
            if (!foundType?.isResolvable()) {
28!
1196
                this.addMultiScopeDiagnostic({
8✔
1197
                    ...DiagnosticMessages.cannotFindName(docTypeTag.typeString),
1198
                    location: brsDocParser.getTypeLocationFromToken(docTypeTag.token) ?? docTypeTag.location
24!
1199
                });
1200
            }
1201
        }
1202
    }
1203

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

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

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

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

1269
            if (isComponentType(type)) {
936✔
1270
                // modify type to allow any member access for Node types
1271
                type.changeUnknownMemberToDynamic = true;
15✔
1272
            }
1273
        }
1274

1275
        // by default return the result of node.getType()
1276
        return type;
8,426✔
1277
    }
1278

1279
    private getParentTypeDescriptor(typeChainResult: TypeChainProcessResult) {
1280
        if (typeChainResult.itemParentTypeKind === BscTypeKind.NamespaceType) {
234✔
1281
            return 'namespace';
117✔
1282
        }
1283
        return 'type';
117✔
1284
    }
1285

1286
    private addDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1287
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
51!
1288
        this.event.program.diagnostics.register(diagnostic, {
51✔
1289
            tags: [diagnosticTag],
1290
            segment: this.currentSegmentBeingValidated
1291
        });
1292
    }
1293

1294
    /**
1295
     * Add a diagnostic (to the first scope) that will have `relatedInformation` for each affected scope
1296
     */
1297
    private addMultiScopeDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1298
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
470✔
1299
        this.event.program.diagnostics.register(diagnostic, {
470✔
1300
            tags: [diagnosticTag],
1301
            segment: this.currentSegmentBeingValidated,
1302
            scope: this.event.scope
1303
        });
1304
    }
1305
}
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