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

rokucommunity / brighterscript / #13685

03 Feb 2025 04:19PM UTC coverage: 86.753% (-1.4%) from 88.185%
#13685

push

web-flow
Merge 34e72243e into 4afb6f658

12476 of 15203 branches covered (82.06%)

Branch coverage included in aggregate %.

7751 of 8408 new or added lines in 101 files covered. (92.19%)

85 existing lines in 17 files now uncovered.

13398 of 14622 relevant lines covered (91.63%)

34302.59 hits per line

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

91.73
/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, isStringTypeLike, 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, CallfuncExpression } 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 type { Location } from 'vscode-languageserver';
34
import { InvalidType } from '../../types/InvalidType';
1✔
35
import { VoidType } from '../../types/VoidType';
1✔
36

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

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

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

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

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

70

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

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

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

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

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

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

113
            if (thisFileHasChanges || this.doesFileProvideChangedSymbol(file, this.event.changedSymbols)) {
2,147✔
114
                this.diagnosticDetectFunctionCollisions(file);
2,002✔
115
            }
116
        });
117

118
        this.event.scope.enumerateOwnFiles((file) => {
1,773✔
119
            if (isBrsFile(file)) {
2,615✔
120

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

124
                const hasUnvalidatedSegments = file.validationSegmenter.hasUnvalidatedSegments();
2,137✔
125

126
                if (hasChangeInfo && !hasUnvalidatedSegments) {
2,137✔
127
                    return;
402✔
128
                }
129

130
                const validationVisitor = createVisitor({
1,735✔
131
                    VariableExpression: (varExpr) => {
132
                        this.validateVariableAndDottedGetExpressions(file, varExpr);
3,918✔
133
                    },
134
                    DottedGetExpression: (dottedGet) => {
135
                        this.validateVariableAndDottedGetExpressions(file, dottedGet);
1,537✔
136
                    },
137
                    CallExpression: (functionCall) => {
138
                        this.validateCallExpression(file, functionCall);
955✔
139
                        this.validateCreateObjectCall(file, functionCall);
955✔
140
                        this.validateComponentMethods(file, functionCall);
955✔
141
                    },
142
                    CallfuncExpression: (functionCall) => {
143
                        this.validateCallFuncExpression(file, functionCall);
48✔
144
                    },
145
                    ReturnStatement: (returnStatement) => {
146
                        this.validateReturnStatement(file, returnStatement);
363✔
147
                    },
148
                    DottedSetStatement: (dottedSetStmt) => {
149
                        this.validateDottedSetStatement(file, dottedSetStmt);
101✔
150
                    },
151
                    BinaryExpression: (binaryExpr) => {
152
                        this.validateBinaryExpression(file, binaryExpr);
279✔
153
                    },
154
                    UnaryExpression: (unaryExpr) => {
155
                        this.validateUnaryExpression(file, unaryExpr);
33✔
156
                    },
157
                    AssignmentStatement: (assignStmt) => {
158
                        this.validateAssignmentStatement(file, assignStmt);
687✔
159
                        // Note: this also includes For statements
160
                        this.detectShadowedLocalVar(file, {
687✔
161
                            expr: assignStmt,
162
                            name: assignStmt.tokens.name.text,
163
                            type: this.getNodeTypeWrapper(file, assignStmt, { flags: SymbolTypeFlag.runtime }),
164
                            nameRange: assignStmt.tokens.name.location?.range
2,061✔
165
                        });
166
                    },
167
                    AugmentedAssignmentStatement: (binaryExpr) => {
168
                        this.validateBinaryExpression(file, binaryExpr);
62✔
169
                    },
170
                    IncrementStatement: (stmt) => {
171
                        this.validateIncrementStatement(file, stmt);
11✔
172
                    },
173
                    NewExpression: (newExpr) => {
174
                        this.validateNewExpression(file, newExpr);
121✔
175
                    },
176
                    ForEachStatement: (forEachStmt) => {
177
                        this.detectShadowedLocalVar(file, {
26✔
178
                            expr: forEachStmt,
179
                            name: forEachStmt.tokens.item.text,
180
                            type: this.getNodeTypeWrapper(file, forEachStmt, { flags: SymbolTypeFlag.runtime }),
181
                            nameRange: forEachStmt.tokens.item.location?.range
78✔
182
                        });
183
                    },
184
                    FunctionParameterExpression: (funcParam) => {
185
                        this.detectShadowedLocalVar(file, {
1,142✔
186
                            expr: funcParam,
187
                            name: funcParam.tokens.name.text,
188
                            type: this.getNodeTypeWrapper(file, funcParam, { flags: SymbolTypeFlag.runtime }),
189
                            nameRange: funcParam.tokens.name.location?.range
3,426✔
190
                        });
191
                    },
192
                    FunctionExpression: (func) => {
193
                        this.validateFunctionExpressionForReturn(func);
2,084✔
194
                    },
195
                    AstNode: (node) => {
196
                        //check for doc comments
197
                        if (!node.leadingTrivia || node.leadingTrivia.filter(triviaToken => triviaToken.kind === TokenKind.Comment).length === 0) {
25,701✔
198
                            return;
19,711✔
199
                        }
200
                        this.validateDocComments(node);
243✔
201
                    }
202
                });
203
                // validate only what's needed in the file
204

205
                const segmentsToWalkForValidation = thisFileHasChanges
1,735✔
206
                    ? file.validationSegmenter.getAllUnvalidatedSegments()
1,735✔
207
                    : file.validationSegmenter.getSegmentsWithChangedSymbols(this.event.changedSymbols);
208

209
                let segmentsValidated = 0;
1,735✔
210
                for (const segment of segmentsToWalkForValidation) {
1,735✔
211
                    if (!file.validationSegmenter.checkIfSegmentNeedsRevalidation(segment, this.event.changedSymbols)) {
3,310!
NEW
212
                        continue;
×
213
                    }
214
                    this.currentSegmentBeingValidated = segment;
3,310✔
215
                    this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, segment: segment, tag: ScopeValidatorDiagnosticTag.Segment });
3,310✔
216
                    segmentsValidated++;
3,310✔
217
                    segment.walk(validationVisitor, {
3,310✔
218
                        walkMode: InsideSegmentWalkMode
219
                    });
220
                    file.markSegmentAsValidated(segment);
3,307✔
221
                    this.currentSegmentBeingValidated = null;
3,307✔
222
                }
223
                this.metrics.set(file.pkgPath, segmentsValidated);
1,732✔
224
            }
225
        });
226
    }
227

228
    private doesFileProvideChangedSymbol(file: BrsFile, changedSymbols: Map<SymbolTypeFlag, Set<string>>) {
229
        if (!changedSymbols) {
153!
NEW
230
            return true;
×
231
        }
232
        for (const flag of [SymbolTypeFlag.runtime, SymbolTypeFlag.typetime]) {
153✔
233
            const providedSymbolKeysFlag = file.providedSymbols.symbolMap.get(flag).keys();
306✔
234
            const changedSymbolSetForFlag = changedSymbols.get(flag);
306✔
235

236
            for (let providedKey of providedSymbolKeysFlag) {
306✔
237
                if (changedSymbolSetForFlag.has(providedKey)) {
284✔
238
                    return true;
8✔
239
                }
240
            }
241
        }
242
        return false;
145✔
243
    }
244

245
    private currentSegmentBeingValidated: AstNode;
246

247

248
    private isTypeKnown(exprType: BscType) {
249
        let isKnownType = exprType?.isResolvable();
4,023✔
250
        return isKnownType;
4,023✔
251
    }
252

253
    /**
254
     * If this is the lhs of an assignment, we don't need to flag it as unresolved
255
     */
256
    private hasValidDeclaration(expression: Expression, exprType: BscType, definingNode?: AstNode) {
257
        if (!isVariableExpression(expression)) {
4,023✔
258
            return false;
1,114✔
259
        }
260
        let assignmentAncestor: AssignmentStatement;
261
        if (isAssignmentStatement(definingNode) && definingNode.tokens.equals.kind === TokenKind.Equal) {
2,909✔
262
            // this symbol was defined in a "normal" assignment (eg. not a compound assignment)
263
            assignmentAncestor = definingNode;
347✔
264
            return assignmentAncestor?.tokens.name?.text.toLowerCase() === expression?.tokens.name?.text.toLowerCase();
347!
265
        } else if (isFunctionParameterExpression(definingNode)) {
2,562✔
266
            // this symbol was defined in a function param
267
            return true;
560✔
268
        } else {
269
            assignmentAncestor = expression?.findAncestor(isAssignmentStatement);
2,002!
270
        }
271
        return assignmentAncestor?.tokens.name === expression?.tokens.name && isUnionType(exprType);
2,002!
272
    }
273

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

282
        //skip non CreateObject function calls
283
        const callName = util.getAllDottedGetPartsAsString(call.callee)?.toLowerCase();
955✔
284
        if (callName !== 'createobject' || !isLiteralExpression(call?.args[0])) {
955!
285
            return;
882✔
286
        }
287
        const firstParamToken = (call?.args[0] as LiteralExpression)?.tokens?.value;
73!
288
        const firstParamStringValue = firstParamToken?.text?.replace(/"/g, '');
73!
289
        if (!firstParamStringValue) {
73!
NEW
290
            return;
×
291
        }
292
        const firstParamStringValueLower = firstParamStringValue.toLowerCase();
73✔
293

294
        //if this is a `createObject('roSGNode'` call, only support known sg node types
295
        if (firstParamStringValueLower === 'rosgnode' && isLiteralExpression(call?.args[1])) {
73!
296
            const componentName: Token = call?.args[1]?.tokens.value;
31!
297
            this.checkComponentName(componentName);
31✔
298
            if (call?.args.length !== 2) {
31!
299
                // roSgNode should only ever have 2 args in `createObject`
300
                this.addDiagnostic({
1✔
301
                    ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, [2], call?.args.length),
3!
302
                    location: call.location
303
                });
304
            }
305
        } else if (!platformComponentNames.has(firstParamStringValueLower)) {
42✔
306
            this.addDiagnostic({
7✔
307
                ...DiagnosticMessages.unknownBrightScriptComponent(firstParamStringValue),
308
                location: firstParamToken.location
309
            });
310
        } else {
311
            // This is valid brightscript component
312
            // Test for invalid arg counts
313
            const brightScriptComponent: BRSComponentData = components[firstParamStringValueLower];
35✔
314
            // Valid arg counts for createObject are 1+ number of args for constructor
315
            let validArgCounts = brightScriptComponent?.constructors.map(cnstr => cnstr.params.length + 1);
36!
316
            if (validArgCounts.length === 0) {
35✔
317
                // no constructors for this component, so createObject only takes 1 arg
318
                validArgCounts = [1];
4✔
319
            }
320
            if (!validArgCounts.includes(call?.args.length)) {
35!
321
                // Incorrect number of arguments included in `createObject()`
322
                this.addDiagnostic({
4✔
323
                    ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, validArgCounts, call?.args.length),
12!
324
                    location: call.location
325
                });
326
            }
327

328
            // Test for deprecation
329
            if (brightScriptComponent?.isDeprecated) {
35!
NEW
330
                this.addDiagnostic({
×
331
                    ...DiagnosticMessages.itemIsDeprecated(firstParamStringValue, brightScriptComponent.deprecatedDescription),
332
                    location: call.location
333
                });
334
            }
335
        }
336

337
    }
338

339
    private checkComponentName(componentName: Token) {
340
        //don't validate any components with a colon in their name (probably component libraries, but regular components can have them too).
341
        if (!componentName || componentName?.text?.includes(':')) {
32!
342
            return;
4✔
343
        }
344
        //add diagnostic for unknown components
345
        const unquotedComponentName = componentName?.text?.replace(/"/g, '');
28!
346
        if (unquotedComponentName && !platformNodeNames.has(unquotedComponentName.toLowerCase()) && !this.event.program.getComponent(unquotedComponentName)) {
28✔
347
            this.addDiagnostic({
4✔
348
                ...DiagnosticMessages.unknownRoSGNode(unquotedComponentName),
349
                location: componentName.location
350
            });
351
        }
352
    }
353

354
    /**
355
     * Validate every method call to `component.callfunc()`, `component.createChild()`, etc.
356
     */
357
    protected validateComponentMethods(file: BrsFile, call: CallExpression) {
358
        const lowerMethodNamesChecked = ['callfunc', 'createchild'];
955✔
359
        if (!isDottedGetExpression(call.callee)) {
955✔
360
            return;
569✔
361
        }
362

363
        const callName = call.callee.tokens?.name?.text?.toLowerCase();
386!
364
        if (!callName || !lowerMethodNamesChecked.includes(callName) || !isLiteralExpression(call?.args[0])) {
386!
365
            return;
373✔
366
        }
367

368
        const callerType = call.callee.obj?.getType({ flags: SymbolTypeFlag.runtime });
13!
369
        if (!isComponentType(callerType)) {
13✔
370
            return;
2✔
371
        }
372
        const firstArgToken = call?.args[0]?.tokens.value;
11!
373
        if (callName === 'createchild') {
11✔
374
            this.checkComponentName(firstArgToken);
1✔
375
        } else if (callName === 'callfunc' && !util.isGenericNodeType(callerType)) {
10✔
376
            const funcType = util.getCallFuncType(call, firstArgToken, { flags: SymbolTypeFlag.runtime, ignoreCall: true });
7✔
377
            if (!funcType?.isResolvable()) {
7✔
378
                const functionName = firstArgToken.text.replace(/"/g, '');
3✔
379
                const functionFullname = `${callerType.toString()}@.${functionName}`;
3✔
380
                this.addMultiScopeDiagnostic({
3✔
381
                    ...DiagnosticMessages.cannotFindCallFuncFunction(functionName, functionFullname, callerType.toString()),
382
                    location: firstArgToken?.location
9!
383
                });
384
            } else {
385
                this.validateFunctionCall(file, funcType, firstArgToken.location, call.args, 1);
4✔
386
            }
387
        }
388
    }
389

390

391
    private validateCallExpression(file: BrsFile, expression: CallExpression) {
392
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: {} };
955✔
393
        let funcType = this.getNodeTypeWrapper(file, expression?.callee, getTypeOptions);
955!
394
        if (funcType?.isResolvable() && isClassType(funcType)) {
955✔
395
            // We're calling a class - get the constructor
396
            funcType = funcType.getMemberType('new', getTypeOptions);
131✔
397
        }
398
        const callErrorLocation = expression?.callee?.location;
955!
399
        return this.validateFunctionCall(file, funcType, callErrorLocation, expression.args);
955✔
400

401
    }
402

403
    private validateCallFuncExpression(file: BrsFile, expression: CallfuncExpression) {
404
        const callerType = expression.callee?.getType({ flags: SymbolTypeFlag.runtime });
48!
405
        if (isDynamicType(callerType)) {
48✔
406
            return;
20✔
407
        }
408
        const methodToken = expression.tokens.methodName;
28✔
409
        const methodName = methodToken?.text ?? '';
28✔
410
        const functionFullname = `${callerType.toString()}@.${methodName}`;
28✔
411
        const callErrorLocation = expression.location;
28✔
412
        if (util.isGenericNodeType(callerType) || isObjectType(callerType) || isDynamicType(callerType)) {
28✔
413
            // ignore "general" node
414
            return;
6✔
415
        }
416

417
        if (!isComponentType(callerType)) {
22✔
418
            this.addMultiScopeDiagnostic({
1✔
419
                ...DiagnosticMessages.cannotFindCallFuncFunction(methodName, functionFullname, callerType.toString()),
420
                location: callErrorLocation
421
            });
422
            return;
1✔
423
        }
424

425
        const funcType = util.getCallFuncType(expression, methodToken, { flags: SymbolTypeFlag.runtime, ignoreCall: true });
21✔
426
        if (!funcType?.isResolvable()) {
18✔
427
            this.addMultiScopeDiagnostic({
1✔
428
                ...DiagnosticMessages.cannotFindCallFuncFunction(methodName, functionFullname, callerType.toString()),
429
                location: callErrorLocation
430
            });
431
        }
432
        return this.validateFunctionCall(file, funcType, callErrorLocation, expression.args);
18✔
433
    }
434

435
    /**
436
     * Detect calls to functions with the incorrect number of parameters, or wrong types of arguments
437
     */
438
    private validateFunctionCall(file: BrsFile, funcType: BscType, callErrorLocation: Location, args: Expression[], argOffset = 0) {
973✔
439
        if (!funcType?.isResolvable() || !isTypedFunctionType(funcType)) {
977✔
440
            return;
266✔
441
        }
442

443
        //get min/max parameter count for callable
444
        let minParams = 0;
711✔
445
        let maxParams = 0;
711✔
446
        for (let param of funcType.params) {
711✔
447
            maxParams++;
987✔
448
            //optional parameters must come last, so we can assume that minParams won't increase once we hit
449
            //the first isOptional
450
            if (param.isOptional !== true) {
987✔
451
                minParams++;
533✔
452
            }
453
        }
454
        if (funcType.isVariadic) {
711✔
455
            // function accepts variable number of arguments
456
            maxParams = CallExpression.MaximumArguments;
12✔
457
        }
458
        const argsForCall = argOffset < 1 ? args : args.slice(argOffset);
711✔
459

460
        let expCallArgCount = argsForCall.length;
711✔
461
        if (expCallArgCount > maxParams || expCallArgCount < minParams) {
711✔
462
            let minMaxParamsText = minParams === maxParams ? maxParams + argOffset : `${minParams + argOffset}-${maxParams + argOffset}`;
33✔
463
            this.addMultiScopeDiagnostic({
33✔
464
                ...DiagnosticMessages.mismatchArgumentCount(minMaxParamsText, expCallArgCount + argOffset),
465
                location: callErrorLocation
466
            });
467
        }
468
        let paramIndex = 0;
711✔
469
        for (let arg of argsForCall) {
711✔
470
            const data = {} as ExtraSymbolData;
621✔
471
            let argType = this.getNodeTypeWrapper(file, arg, { flags: SymbolTypeFlag.runtime, data: data });
621✔
472

473
            const paramType = funcType.params[paramIndex]?.type;
621✔
474
            if (!paramType) {
621✔
475
                // unable to find a paramType -- maybe there are more args than params
476
                break;
22✔
477
            }
478

479
            if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) {
599✔
480
                argType = data.definingNode.getConstructorType();
2✔
481
            }
482

483
            const compatibilityData: TypeCompatibilityData = {};
599✔
484
            const isAllowedArgConversion = this.checkAllowedArgConversions(paramType, argType);
599✔
485
            if (!isAllowedArgConversion && !paramType?.isTypeCompatible(argType, compatibilityData)) {
599!
486
                this.addMultiScopeDiagnostic({
42✔
487
                    ...DiagnosticMessages.argumentTypeMismatch(argType.toString(), paramType.toString(), compatibilityData),
488
                    location: arg.location
489
                });
490
            }
491
            paramIndex++;
599✔
492
        }
493
    }
494

495
    private checkAllowedArgConversions(paramType: BscType, argType: BscType): boolean {
496
        if (isNumberType(argType) && isBooleanType(paramType)) {
599✔
497
            return true;
8✔
498
        }
499
        return false;
591✔
500
    }
501

502

503
    /**
504
     * Detect return statements with incompatible types vs. declared return type
505
     */
506
    private validateReturnStatement(file: BrsFile, returnStmt: ReturnStatement) {
507
        const data: ExtraSymbolData = {};
364✔
508
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: data };
364✔
509
        let funcType = returnStmt.findAncestor(isFunctionExpression)?.getType({ flags: SymbolTypeFlag.typetime });
364✔
510
        if (isTypedFunctionType(funcType)) {
364✔
511
            let actualReturnType = returnStmt?.value
363!
512
                ? this.getNodeTypeWrapper(file, returnStmt?.value, getTypeOptions)
1,413!
513
                : VoidType.instance;
514
            const compatibilityData: TypeCompatibilityData = {};
363✔
515

516
            // `return` statement by itself in non-built-in function will actually result in `invalid`
517
            const valueReturnType = isVoidType(actualReturnType) ? InvalidType.instance : actualReturnType;
363✔
518

519
            if (funcType.returnType.isResolvable()) {
363✔
520
                if (!returnStmt?.value && isVoidType(funcType.returnType)) {
359!
521
                    // allow empty return when function is return `as void`
522
                    // eslint-disable-next-line no-useless-return
523
                    return;
9✔
524
                } else if (!funcType.returnType.isTypeCompatible(valueReturnType, compatibilityData)) {
350✔
525
                    this.addMultiScopeDiagnostic({
14✔
526
                        ...DiagnosticMessages.returnTypeMismatch(actualReturnType.toString(), funcType.returnType.toString(), compatibilityData),
527
                        location: returnStmt.value?.location ?? returnStmt.location
84✔
528
                    });
529
                }
530
            }
531
        }
532
    }
533

534
    /**
535
     * Detect assigned type different from expected member type
536
     */
537
    private validateDottedSetStatement(file: BrsFile, dottedSetStmt: DottedSetStatement) {
538
        const typeChainExpectedLHS = [] as TypeChainEntry[];
101✔
539
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
101✔
540

541
        const expectedLHSType = this.getNodeTypeWrapper(file, dottedSetStmt, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
101✔
542
        const actualRHSType = this.getNodeTypeWrapper(file, dottedSetStmt?.value, getTypeOpts);
101!
543
        const compatibilityData: TypeCompatibilityData = {};
101✔
544
        const typeChainScan = util.processTypeChain(typeChainExpectedLHS);
101✔
545
        // check if anything in typeChain is an AA - if so, just allow it
546
        if (typeChainExpectedLHS.find(typeChainItem => isAssociativeArrayType(typeChainItem.type))) {
177✔
547
            // something in the chain is an AA
548
            // treat members as dynamic - they could have been set without the type system's knowledge
549
            return;
39✔
550
        }
551
        if (!expectedLHSType || !expectedLHSType?.isResolvable()) {
62!
552
            this.addMultiScopeDiagnostic({
5✔
553
                ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
554
                location: typeChainScan?.location
15!
555
            });
556
            return;
5✔
557
        }
558

559
        let accessibilityIsOk = this.checkMemberAccessibility(file, dottedSetStmt, typeChainExpectedLHS);
57✔
560

561
        //Most Component fields can be set with strings
562
        //TODO: be more precise about which fields can actually accept strings
563
        //TODO: if RHS is a string literal, we can do more validation to make sure it's the correct type
564
        if (isComponentType(dottedSetStmt.obj?.getType({ flags: SymbolTypeFlag.runtime }))) {
57!
565
            if (isStringTypeLike(actualRHSType)) {
21✔
566
                return;
6✔
567
            }
568
        }
569

570
        if (accessibilityIsOk && !expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
51!
571
            this.addMultiScopeDiagnostic({
12✔
572
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
573
                location: dottedSetStmt.location
574
            });
575
        }
576
    }
577

578
    /**
579
     * Detect when declared type does not match rhs type
580
     */
581
    private validateAssignmentStatement(file: BrsFile, assignStmt: AssignmentStatement) {
582
        if (!assignStmt?.typeExpression) {
687!
583
            // nothing to check
584
            return;
680✔
585
        }
586

587
        const typeChainExpectedLHS = [];
7✔
588
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
7✔
589
        const expectedLHSType = this.getNodeTypeWrapper(file, assignStmt.typeExpression, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
7✔
590
        const actualRHSType = this.getNodeTypeWrapper(file, assignStmt.value, getTypeOpts);
7✔
591
        const compatibilityData: TypeCompatibilityData = {};
7✔
592
        if (!expectedLHSType || !expectedLHSType.isResolvable()) {
7✔
593
            // LHS is not resolvable... handled elsewhere
594
        } else if (!expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
6!
595
            this.addMultiScopeDiagnostic({
1✔
596
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
597
                location: assignStmt.location
598
            });
599
        }
600
    }
601

602
    /**
603
     * Detect invalid use of a binary operator
604
     */
605
    private validateBinaryExpression(file: BrsFile, binaryExpr: BinaryExpression | AugmentedAssignmentStatement) {
606
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
341✔
607

608
        if (util.isInTypeExpression(binaryExpr)) {
341✔
609
            return;
13✔
610
        }
611

612
        let leftType = isBinaryExpression(binaryExpr)
328✔
613
            ? this.getNodeTypeWrapper(file, binaryExpr.left, getTypeOpts)
328✔
614
            : this.getNodeTypeWrapper(file, binaryExpr.item, getTypeOpts);
615
        let rightType = isBinaryExpression(binaryExpr)
328✔
616
            ? this.getNodeTypeWrapper(file, binaryExpr.right, getTypeOpts)
328✔
617
            : this.getNodeTypeWrapper(file, binaryExpr.value, getTypeOpts);
618

619
        if (!leftType.isResolvable() || !rightType.isResolvable()) {
328✔
620
            // Can not find the type. error handled elsewhere
621
            return;
13✔
622
        }
623

624
        let leftTypeToTest = leftType;
315✔
625
        let rightTypeToTest = rightType;
315✔
626

627
        if (isEnumMemberType(leftType) || isEnumType(leftType)) {
315✔
628
            leftTypeToTest = leftType.underlyingType;
11✔
629
        }
630
        if (isEnumMemberType(rightType) || isEnumType(rightType)) {
315✔
631
            rightTypeToTest = rightType.underlyingType;
10✔
632
        }
633

634
        if (isUnionType(leftType) || isUnionType(rightType)) {
315✔
635
            // TODO: it is possible to validate based on innerTypes, but more complicated
636
            // Because you need to verify each combination of types
637
            return;
26✔
638
        }
639
        const opResult = util.binaryOperatorResultType(leftTypeToTest, binaryExpr.tokens.operator, rightTypeToTest);
289✔
640

641
        if (!opResult) {
289✔
642
            // if the result was dynamic or void, that means there wasn't a valid operation
643
            this.addMultiScopeDiagnostic({
9✔
644
                ...DiagnosticMessages.operatorTypeMismatch(binaryExpr.tokens.operator.text, leftType.toString(), rightType.toString()),
645
                location: binaryExpr.location
646
            });
647
        }
648
    }
649

650
    /**
651
     * Detect invalid use of a Unary operator
652
     */
653
    private validateUnaryExpression(file: BrsFile, unaryExpr: UnaryExpression) {
654
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
33✔
655

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

658
        if (!rightType.isResolvable()) {
33!
659
            // Can not find the type. error handled elsewhere
NEW
660
            return;
×
661
        }
662
        let rightTypeToTest = rightType;
33✔
663
        if (isEnumMemberType(rightType)) {
33!
NEW
664
            rightTypeToTest = rightType.underlyingType;
×
665
        }
666

667
        if (isUnionType(rightTypeToTest)) {
33✔
668
            // TODO: it is possible to validate based on innerTypes, but more complicated
669
            // Because you need to verify each combination of types
670

671
        } else if (isDynamicType(rightTypeToTest) || isObjectType(rightTypeToTest)) {
32✔
672
            // operand is basically "any" type... ignore;
673

674
        } else if (isPrimitiveType(rightType)) {
29!
675
            const opResult = util.unaryOperatorResultType(unaryExpr.tokens.operator, rightTypeToTest);
29✔
676
            if (!opResult) {
29✔
677
                this.addMultiScopeDiagnostic({
1✔
678
                    ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
679
                    location: unaryExpr.location
680
                });
681
            }
682
        } else {
683
            // rhs is not a primitive, so no binary operator is allowed
NEW
684
            this.addMultiScopeDiagnostic({
×
685
                ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
686
                location: unaryExpr.location
687
            });
688
        }
689
    }
690

691
    private validateIncrementStatement(file: BrsFile, incStmt: IncrementStatement) {
692
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
11✔
693

694
        let rightType = this.getNodeTypeWrapper(file, incStmt.value, getTypeOpts);
11✔
695

696
        if (!rightType.isResolvable()) {
11!
697
            // Can not find the type. error handled elsewhere
NEW
698
            return;
×
699
        }
700

701
        if (isUnionType(rightType)) {
11✔
702
            // TODO: it is possible to validate based on innerTypes, but more complicated
703
            // because you need to verify each combination of types
704
        } else if (isDynamicType(rightType) || isObjectType(rightType)) {
9✔
705
            // operand is basically "any" type... ignore
706
        } else if (isNumberType(rightType)) {
8✔
707
            // operand is a number.. this is ok
708
        } else {
709
            // rhs is not a number, so no increment operator is not allowed
710
            this.addMultiScopeDiagnostic({
1✔
711
                ...DiagnosticMessages.operatorTypeMismatch(incStmt.tokens.operator.text, rightType.toString()),
712
                location: incStmt.location
713
            });
714
        }
715
    }
716

717

718
    validateVariableAndDottedGetExpressions(file: BrsFile, expression: VariableExpression | DottedGetExpression) {
719
        if (isDottedGetExpression(expression.parent)) {
5,455✔
720
            // We validate dottedGetExpressions at the top-most level
721
            return;
1,429✔
722
        }
723
        if (isVariableExpression(expression)) {
4,026✔
724
            if (isAssignmentStatement(expression.parent) && expression.parent.tokens.name === expression.tokens.name) {
2,912!
725
                // Don't validate LHS of assignments
NEW
726
                return;
×
727
            } else if (isNamespaceStatement(expression.parent)) {
2,912✔
728
                return;
3✔
729
            }
730
        }
731

732
        let symbolType = SymbolTypeFlag.runtime;
4,023✔
733
        let oppositeSymbolType = SymbolTypeFlag.typetime;
4,023✔
734
        const isUsedAsType = util.isInTypeExpression(expression);
4,023✔
735
        if (isUsedAsType) {
4,023✔
736
            // This is used in a TypeExpression - only look up types from SymbolTable
737
            symbolType = SymbolTypeFlag.typetime;
1,277✔
738
            oppositeSymbolType = SymbolTypeFlag.runtime;
1,277✔
739
        }
740

741
        // Do a complete type check on all DottedGet and Variable expressions
742
        // this will create a diagnostic if an invalid member is accessed
743
        const typeChain: TypeChainEntry[] = [];
4,023✔
744
        const typeData = {} as ExtraSymbolData;
4,023✔
745
        let exprType = this.getNodeTypeWrapper(file, expression, {
4,023✔
746
            flags: symbolType,
747
            typeChain: typeChain,
748
            data: typeData
749
        });
750

751
        const hasValidDeclaration = this.hasValidDeclaration(expression, exprType, typeData?.definingNode);
4,023!
752

753
        //include a hint diagnostic if this type is marked as deprecated
754
        if (typeData.flags & SymbolTypeFlag.deprecated) { // eslint-disable-line no-bitwise
4,023✔
755
            this.addMultiScopeDiagnostic({
2✔
756
                ...DiagnosticMessages.itemIsDeprecated(),
757
                location: expression.tokens.name.location,
758
                tags: [DiagnosticTag.Deprecated]
759
            });
760
        }
761

762
        if (!this.isTypeKnown(exprType) && !hasValidDeclaration) {
4,023✔
763
            if (this.getNodeTypeWrapper(file, expression, { flags: oppositeSymbolType, isExistenceTest: true })?.isResolvable()) {
236!
764
                const oppoSiteTypeChain = [];
5✔
765
                const invalidlyUsedResolvedType = this.getNodeTypeWrapper(file, expression, { flags: oppositeSymbolType, typeChain: oppoSiteTypeChain, isExistenceTest: true });
5✔
766
                const typeChainScan = util.processTypeChain(oppoSiteTypeChain);
5✔
767
                if (isUsedAsType) {
5✔
768
                    this.addMultiScopeDiagnostic({
2✔
769
                        ...DiagnosticMessages.itemCannotBeUsedAsType(typeChainScan.fullChainName),
770
                        location: expression.location
771
                    });
772
                } else if (invalidlyUsedResolvedType && !isReferenceType(invalidlyUsedResolvedType)) {
3✔
773
                    if (!isAliasStatement(expression.parent)) {
1!
774
                        // alias rhs CAN be a type!
NEW
775
                        this.addMultiScopeDiagnostic({
×
776
                            ...DiagnosticMessages.itemCannotBeUsedAsVariable(invalidlyUsedResolvedType.toString()),
777
                            location: expression.location
778
                        });
779
                    }
780
                } else {
781
                    const typeChainScan = util.processTypeChain(typeChain);
2✔
782
                    //if this is a function call, provide a different diagnostic code
783
                    if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
2✔
784
                        this.addMultiScopeDiagnostic({
1✔
785
                            ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
786
                            location: typeChainScan?.location
3!
787
                        });
788
                    } else {
789
                        this.addMultiScopeDiagnostic({
1✔
790
                            ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
791
                            location: typeChainScan?.location
3!
792
                        });
793
                    }
794
                }
795

796
            } else if (!(typeData?.isFromDocComment)) {
231!
797
                // only show "cannot find... " errors if the type is not defined from a doc comment
798
                const typeChainScan = util.processTypeChain(typeChain);
229✔
799
                if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
229✔
800
                    this.addMultiScopeDiagnostic({
27✔
801
                        ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
802
                        location: typeChainScan?.location
81!
803
                    });
804
                } else {
805
                    this.addMultiScopeDiagnostic({
202✔
806
                        ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
807
                        location: typeChainScan?.location
606!
808
                    });
809
                }
810

811
            }
812
        }
813
        if (isUsedAsType) {
4,023✔
814
            return;
1,277✔
815
        }
816

817
        const containingNamespace = expression.findAncestor<NamespaceStatement>(isNamespaceStatement);
2,746✔
818
        const containingNamespaceName = containingNamespace?.getName(ParseMode.BrighterScript);
2,746✔
819

820
        if (!(isCallExpression(expression.parent) && isNewExpression(expression.parent?.parent))) {
2,746!
821
            const classUsedAsVarEntry = this.checkTypeChainForClassUsedAsVar(typeChain, containingNamespaceName);
2,625✔
822
            const isClassInNamespace = containingNamespace?.getSymbolTable().hasSymbol(typeChain[0].name, SymbolTypeFlag.runtime);
2,625✔
823
            if (classUsedAsVarEntry && !isClassInNamespace) {
2,625!
824

NEW
825
                this.addMultiScopeDiagnostic({
×
826
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable(classUsedAsVarEntry.toString()),
827
                    location: expression.location
828
                });
NEW
829
                return;
×
830
            }
831
        }
832

833
        const lastTypeInfo = typeChain[typeChain.length - 1];
2,746✔
834
        const parentTypeInfo = typeChain[typeChain.length - 2];
2,746✔
835

836
        this.checkMemberAccessibility(file, expression, typeChain);
2,746✔
837

838
        if (isNamespaceType(exprType) && !isAliasStatement(expression.parent)) {
2,746✔
839
            this.addMultiScopeDiagnostic({
24✔
840
                ...DiagnosticMessages.itemCannotBeUsedAsVariable('namespace'),
841
                location: expression.location
842
            });
843
        } else if (isEnumType(exprType) && !isAliasStatement(expression.parent)) {
2,722✔
844
            const enumStatement = this.event.scope.getEnum(util.getAllDottedGetPartsAsString(expression));
25✔
845
            if (enumStatement) {
25✔
846
                // there's an enum with this name
847
                this.addMultiScopeDiagnostic({
2✔
848
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable('enum'),
849
                    location: expression.location
850
                });
851
            }
852
        } else if (isDynamicType(exprType) && isEnumType(parentTypeInfo?.type) && isDottedGetExpression(expression)) {
2,697✔
853
            const enumFileLink = this.event.scope.getEnumFileLink(util.getAllDottedGetPartsAsString(expression.obj));
9✔
854
            const typeChainScanForItem = util.processTypeChain(typeChain);
9✔
855
            const typeChainScanForParent = util.processTypeChain(typeChain.slice(0, -1));
9✔
856
            if (enumFileLink) {
9✔
857
                this.addMultiScopeDiagnostic({
5✔
858
                    ...DiagnosticMessages.cannotFindName(lastTypeInfo?.name, typeChainScanForItem.fullChainName, typeChainScanForParent.fullNameOfItem, 'enum'),
15!
859
                    location: lastTypeInfo?.location,
15!
860
                    relatedInformation: [{
861
                        message: 'Enum declared here',
862
                        location: util.createLocationFromRange(
863
                            util.pathToUri(enumFileLink?.file.srcPath),
15!
864
                            enumFileLink?.item?.tokens.name.location?.range
45!
865
                        )
866
                    }]
867
                });
868
            }
869
        }
870
    }
871

872
    private checkTypeChainForClassUsedAsVar(typeChain: TypeChainEntry[], containingNamespaceName: string) {
873
        const ignoreKinds = [AstNodeKind.TypecastExpression, AstNodeKind.NewExpression];
2,625✔
874
        let lowerNameSoFar = '';
2,625✔
875
        let classUsedAsVar;
876
        let isFirst = true;
2,625✔
877
        for (let i = 0; i < typeChain.length - 1; i++) { // do not look at final entry - we CAN use the constructor as a variable
2,625✔
878
            const tce = typeChain[i];
1,336✔
879
            lowerNameSoFar += `${lowerNameSoFar ? '.' : ''}${tce.name.toLowerCase()}`;
1,336✔
880
            if (!isNamespaceType(tce.type)) {
1,336✔
881
                if (isFirst && containingNamespaceName) {
666✔
882
                    lowerNameSoFar = `${containingNamespaceName.toLowerCase()}.${lowerNameSoFar}`;
75✔
883
                }
884
                if (!tce.astNode || ignoreKinds.includes(tce.astNode.kind)) {
666✔
885
                    break;
14✔
886
                } else if (isClassType(tce.type) && lowerNameSoFar.toLowerCase() === tce.type.name.toLowerCase()) {
652✔
887
                    classUsedAsVar = tce.type;
1✔
888
                }
889
                break;
652✔
890
            }
891
            isFirst = false;
670✔
892
        }
893

894
        return classUsedAsVar;
2,625✔
895
    }
896

897
    /**
898
     * Adds diagnostics for accibility mismatches
899
     *
900
     * @param file file
901
     * @param expression containing expression
902
     * @param typeChain type chain to check
903
     * @returns true if member accesiibility is okay
904
     */
905
    private checkMemberAccessibility(file: BscFile, expression: Expression, typeChain: TypeChainEntry[]) {
906
        for (let i = 0; i < typeChain.length - 1; i++) {
2,803✔
907
            const parentChainItem = typeChain[i];
1,513✔
908
            const childChainItem = typeChain[i + 1];
1,513✔
909
            if (isClassType(parentChainItem.type)) {
1,513✔
910
                const containingClassStmt = expression.findAncestor<ClassStatement>(isClassStatement);
159✔
911
                const classStmtThatDefinesChildMember = childChainItem.data?.definingNode?.findAncestor<ClassStatement>(isClassStatement);
159!
912
                if (classStmtThatDefinesChildMember) {
159✔
913
                    const definingClassName = classStmtThatDefinesChildMember.getName(ParseMode.BrighterScript);
157✔
914
                    const inMatchingClassStmt = containingClassStmt?.getName(ParseMode.BrighterScript).toLowerCase() === parentChainItem.type.name.toLowerCase();
157✔
915
                    // eslint-disable-next-line no-bitwise
916
                    if (childChainItem.data.flags & SymbolTypeFlag.private) {
157✔
917
                        if (!inMatchingClassStmt || childChainItem.data.memberOfAncestor) {
16✔
918
                            this.addMultiScopeDiagnostic({
4✔
919
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
920
                                location: expression.location
921
                            });
922
                            // there's an error... don't worry about the rest of the chain
923
                            return false;
4✔
924
                        }
925
                    }
926

927
                    // eslint-disable-next-line no-bitwise
928
                    if (childChainItem.data.flags & SymbolTypeFlag.protected) {
153✔
929
                        const containingClassName = containingClassStmt?.getName(ParseMode.BrighterScript);
13✔
930
                        const containingNamespaceName = expression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
13✔
931
                        const ancestorClasses = this.event.scope.getClassHierarchy(containingClassName, containingNamespaceName).map(link => link.item);
13✔
932
                        const isSubClassOfDefiningClass = ancestorClasses.includes(classStmtThatDefinesChildMember);
13✔
933

934
                        if (!isSubClassOfDefiningClass) {
13✔
935
                            this.addMultiScopeDiagnostic({
5✔
936
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
937
                                location: expression.location
938
                            });
939
                            // there's an error... don't worry about the rest of the chain
940
                            return false;
5✔
941
                        }
942
                    }
943
                }
944

945
            }
946
        }
947
        return true;
2,794✔
948
    }
949

950
    /**
951
     * Find all "new" statements in the program,
952
     * and make sure we can find a class with that name
953
     */
954
    private validateNewExpression(file: BrsFile, newExpression: NewExpression) {
955
        const newExprType = this.getNodeTypeWrapper(file, newExpression, { flags: SymbolTypeFlag.typetime });
121✔
956
        if (isClassType(newExprType)) {
121✔
957
            return;
113✔
958
        }
959

960
        let potentialClassName = newExpression.className.getName(ParseMode.BrighterScript);
8✔
961
        const namespaceName = newExpression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
8!
962
        let newableClass = this.event.scope.getClass(potentialClassName, namespaceName);
8✔
963

964
        if (!newableClass) {
8!
965
            //try and find functions with this name.
966
            let fullName = util.getFullyQualifiedClassName(potentialClassName, namespaceName);
8✔
967

968
            this.addMultiScopeDiagnostic({
8✔
969
                ...DiagnosticMessages.expressionIsNotConstructable(fullName),
970
                location: newExpression.className.location
971
            });
972

973
        }
974
    }
975

976
    private validateFunctionExpressionForReturn(func: FunctionExpression) {
977
        const returnType = func?.returnTypeExpression?.getType({ flags: SymbolTypeFlag.typetime });
2,084!
978

979
        if (!returnType || !returnType.isResolvable() || isVoidType(returnType) || isDynamicType(returnType)) {
2,084✔
980
            return;
1,855✔
981
        }
982
        const returns = func.body?.findChild<ReturnStatement>(isReturnStatement, { walkMode: WalkMode.visitAll });
229!
983
        if (!returns && isStringTypeLike(returnType)) {
229✔
984
            this.addMultiScopeDiagnostic({
5✔
985
                ...DiagnosticMessages.returnTypeCoercionMismatch(returnType.toString()),
986
                location: func.location
987
            });
988
        }
989
    }
990

991
    /**
992
     * Create diagnostics for any duplicate function declarations
993
     */
994
    private flagDuplicateFunctionDeclarations() {
995
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration });
1,770✔
996

997
        //for each list of callables with the same name
998
        for (let [lowerName, callableContainers] of this.event.scope.getCallableContainerMap()) {
1,770✔
999

1000
            let globalCallables = [] as CallableContainer[];
132,905✔
1001
            let nonGlobalCallables = [] as CallableContainer[];
132,905✔
1002
            let ownCallables = [] as CallableContainer[];
132,905✔
1003
            let ancestorNonGlobalCallables = [] as CallableContainer[];
132,905✔
1004

1005

1006
            for (let container of callableContainers) {
132,905✔
1007
                if (container.scope === this.event.program.globalScope) {
140,019✔
1008
                    globalCallables.push(container);
138,060✔
1009
                } else {
1010
                    nonGlobalCallables.push(container);
1,959✔
1011
                    if (container.scope === this.event.scope) {
1,959✔
1012
                        ownCallables.push(container);
1,929✔
1013
                    } else {
1014
                        ancestorNonGlobalCallables.push(container);
30✔
1015
                    }
1016
                }
1017
            }
1018

1019
            //add info diagnostics about child shadowing parent functions
1020
            if (ownCallables.length > 0 && ancestorNonGlobalCallables.length > 0) {
132,905✔
1021
                for (let container of ownCallables) {
24✔
1022
                    //skip the init function (because every component will have one of those){
1023
                    if (lowerName !== 'init') {
24✔
1024
                        let shadowedCallable = ancestorNonGlobalCallables[ancestorNonGlobalCallables.length - 1];
23✔
1025
                        if (!!shadowedCallable && shadowedCallable.callable.file === container.callable.file) {
23✔
1026
                            //same file: skip redundant imports
1027
                            continue;
20✔
1028
                        }
1029
                        this.addMultiScopeDiagnostic({
3✔
1030
                            ...DiagnosticMessages.overridesAncestorFunction(
1031
                                container.callable.name,
1032
                                container.scope.name,
1033
                                shadowedCallable.callable.file.destPath,
1034
                                //grab the last item in the list, which should be the closest ancestor's version
1035
                                shadowedCallable.scope.name
1036
                            ),
1037
                            location: util.createLocationFromFileRange(container.callable.file, container.callable.nameRange)
1038
                        }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
1039
                    }
1040
                }
1041
            }
1042

1043
            //add error diagnostics about duplicate functions in the same scope
1044
            if (ownCallables.length > 1) {
132,905✔
1045

1046
                for (let callableContainer of ownCallables) {
5✔
1047
                    let callable = callableContainer.callable;
10✔
1048
                    const related = [];
10✔
1049
                    for (const ownCallable of ownCallables) {
10✔
1050
                        const thatNameRange = ownCallable.callable.nameRange;
20✔
1051
                        if (ownCallable.callable.nameRange !== callable.nameRange) {
20✔
1052
                            related.push({
10✔
1053
                                message: `Function declared here`,
1054
                                location: util.createLocationFromRange(
1055
                                    util.pathToUri(ownCallable.callable.file?.srcPath),
30!
1056
                                    thatNameRange
1057
                                )
1058
                            });
1059
                        }
1060
                    }
1061

1062
                    this.addMultiScopeDiagnostic({
10✔
1063
                        ...DiagnosticMessages.duplicateFunctionImplementation(callable.name),
1064
                        location: util.createLocationFromFileRange(callable.file, callable.nameRange),
1065
                        relatedInformation: related
1066
                    }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
1067
                }
1068
            }
1069
        }
1070
    }
1071

1072
    /**
1073
     * Verify that all of the scripts imported by each file in this scope actually exist, and have the correct case
1074
     */
1075
    private validateScriptImportPaths() {
1076
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Imports });
1,770✔
1077

1078
        let scriptImports = this.event.scope.getOwnScriptImports();
1,770✔
1079
        //verify every script import
1080
        for (let scriptImport of scriptImports) {
1,770✔
1081
            let referencedFile = this.event.scope.getFileByRelativePath(scriptImport.destPath);
540✔
1082
            //if we can't find the file
1083
            if (!referencedFile) {
540✔
1084
                //skip the default bslib file, it will exist at transpile time but should not show up in the program during validation cycle
1085
                if (scriptImport.destPath === this.event.program.bslibPkgPath) {
16✔
1086
                    continue;
2✔
1087
                }
1088
                let dInfo: DiagnosticInfo;
1089
                if (scriptImport.text.trim().length === 0) {
14✔
1090
                    dInfo = DiagnosticMessages.scriptSrcCannotBeEmpty();
1✔
1091
                } else {
1092
                    dInfo = DiagnosticMessages.referencedFileDoesNotExist();
13✔
1093
                }
1094

1095
                this.addMultiScopeDiagnostic({
14✔
1096
                    ...dInfo,
1097
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
1098
                }, ScopeValidatorDiagnosticTag.Imports);
1099
                //if the character casing of the script import path does not match that of the actual path
1100
            } else if (scriptImport.destPath !== referencedFile.destPath) {
524✔
1101
                this.addMultiScopeDiagnostic({
2✔
1102
                    ...DiagnosticMessages.scriptImportCaseMismatch(referencedFile.destPath),
1103
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
1104
                }, ScopeValidatorDiagnosticTag.Imports);
1105
            }
1106
        }
1107
    }
1108

1109
    /**
1110
     * Validate all classes defined in this scope
1111
     */
1112
    private validateClasses() {
1113
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Classes });
1,770✔
1114

1115
        let validator = new BsClassValidator(this.event.scope);
1,770✔
1116
        validator.validate();
1,770✔
1117
        for (const diagnostic of validator.diagnostics) {
1,770✔
1118
            this.addMultiScopeDiagnostic({
29✔
1119
                ...diagnostic
1120
            }, ScopeValidatorDiagnosticTag.Classes);
1121
        }
1122
    }
1123

1124

1125
    /**
1126
     * Find various function collisions
1127
     */
1128
    private diagnosticDetectFunctionCollisions(file: BrsFile) {
1129
        const fileUri = util.pathToUri(file.srcPath);
2,002✔
1130
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, tag: ScopeValidatorDiagnosticTag.FunctionCollisions });
2,002✔
1131
        for (let func of file.callables) {
2,002✔
1132
            const funcName = func.getName(ParseMode.BrighterScript);
1,799✔
1133
            const lowerFuncName = funcName?.toLowerCase();
1,799!
1134
            if (lowerFuncName) {
1,799!
1135

1136
                //find function declarations with the same name as a stdlib function
1137
                if (globalCallableMap.has(lowerFuncName)) {
1,799✔
1138
                    this.addMultiScopeDiagnostic({
5✔
1139
                        ...DiagnosticMessages.scopeFunctionShadowedByBuiltInFunction(),
1140
                        location: util.createLocationFromRange(fileUri, func.nameRange)
1141

1142
                    });
1143
                }
1144
            }
1145
        }
1146
    }
1147

1148
    public detectShadowedLocalVar(file: BrsFile, varDeclaration: { expr: AstNode; name: string; type: BscType; nameRange: Range }) {
1149
        const varName = varDeclaration.name;
1,855✔
1150
        const lowerVarName = varName.toLowerCase();
1,855✔
1151
        const callableContainerMap = this.event.scope.getCallableContainerMap();
1,855✔
1152
        const containingNamespace = varDeclaration.expr?.findAncestor<NamespaceStatement>(isNamespaceStatement);
1,855!
1153
        const localVarIsInNamespace = util.isVariableMemberOfNamespace(varDeclaration.name, varDeclaration.expr, containingNamespace);
1,855✔
1154

1155
        const varIsFunction = () => {
1,855✔
1156
            return isCallableType(varDeclaration.type);
11✔
1157
        };
1158

1159
        if (
1,855✔
1160
            //has same name as stdlib
1161
            globalCallableMap.has(lowerVarName)
1162
        ) {
1163
            //local var function with same name as stdlib function
1164
            if (varIsFunction()) {
8✔
1165
                this.addMultiScopeDiagnostic({
1✔
1166
                    ...DiagnosticMessages.localVarFunctionShadowsParentFunction('stdlib'),
1167
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange)
1168
                });
1169
            }
1170
        } else if (callableContainerMap.has(lowerVarName) && !localVarIsInNamespace) {
1,847✔
1171
            const callable = callableContainerMap.get(lowerVarName);
3✔
1172
            //is same name as a callable
1173
            if (varIsFunction()) {
3✔
1174
                this.addMultiScopeDiagnostic({
1✔
1175
                    ...DiagnosticMessages.localVarFunctionShadowsParentFunction('scope'),
1176
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1177
                    relatedInformation: [{
1178
                        message: 'Function declared here',
1179
                        location: util.createLocationFromFileRange(
1180
                            callable[0].callable.file,
1181
                            callable[0].callable.nameRange
1182
                        )
1183
                    }]
1184
                });
1185
            } else {
1186
                this.addMultiScopeDiagnostic({
2✔
1187
                    ...DiagnosticMessages.localVarShadowedByScopedFunction(),
1188
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1189
                    relatedInformation: [{
1190
                        message: 'Function declared here',
1191
                        location: util.createLocationFromRange(
1192
                            util.pathToUri(callable[0].callable.file.srcPath),
1193
                            callable[0].callable.nameRange
1194
                        )
1195
                    }]
1196
                });
1197
            }
1198
            //has the same name as an in-scope class
1199
        } else if (!localVarIsInNamespace) {
1,844✔
1200
            const classStmtLink = this.event.scope.getClassFileLink(lowerVarName);
1,843✔
1201
            if (classStmtLink) {
1,843✔
1202
                this.addMultiScopeDiagnostic({
3✔
1203
                    ...DiagnosticMessages.localVarShadowedByScopedFunction(),
1204
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1205
                    relatedInformation: [{
1206
                        message: 'Class declared here',
1207
                        location: util.createLocationFromRange(
1208
                            util.pathToUri(classStmtLink.file.srcPath),
1209
                            classStmtLink?.item.tokens.name.location?.range
18!
1210
                        )
1211
                    }]
1212
                });
1213
            }
1214
        }
1215
    }
1216

1217
    private validateXmlInterface(scope: XmlScope) {
1218
        if (!scope.xmlFile.parser.ast?.componentElement?.interfaceElement) {
478!
1219
            return;
406✔
1220
        }
1221
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLInterface });
72!
1222

1223
        const iface = scope.xmlFile.parser.ast.componentElement.interfaceElement;
72✔
1224
        const callableContainerMap = scope.getCallableContainerMap();
72✔
1225
        //validate functions
1226
        for (const func of iface.functions) {
72✔
1227
            const name = func.name;
62✔
1228
            if (!name) {
62✔
1229
                this.addDiagnostic({
3✔
1230
                    ...DiagnosticMessages.xmlTagMissingAttribute(func.tokens.startTagName.text, 'name'),
1231
                    location: func.tokens.startTagName.location
1232
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1233
            } else if (!callableContainerMap.has(name.toLowerCase())) {
59✔
1234
                this.addDiagnostic({
4✔
1235
                    ...DiagnosticMessages.xmlFunctionNotFound(name),
1236
                    location: func.getAttribute('name')?.tokens.value.location
12!
1237
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1238
            }
1239
        }
1240
        //validate fields
1241
        for (const field of iface.fields) {
72✔
1242
            const { id, type, onChange } = field;
41✔
1243
            if (!id) {
41✔
1244
                this.addDiagnostic({
3✔
1245
                    ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'id'),
1246
                    location: field.tokens.startTagName.location
1247
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1248
            }
1249
            if (!type) {
41✔
1250
                if (!field.alias) {
3✔
1251
                    this.addDiagnostic({
2✔
1252
                        ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'type'),
1253
                        location: field.tokens.startTagName.location
1254
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1255
                }
1256
            } else if (!SGFieldTypes.includes(type.toLowerCase())) {
38✔
1257
                this.addDiagnostic({
1✔
1258
                    ...DiagnosticMessages.xmlInvalidFieldType(type),
1259
                    location: field.getAttribute('type')?.tokens.value.location
3!
1260
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1261
            }
1262
            if (onChange) {
41✔
1263
                if (!callableContainerMap.has(onChange.toLowerCase())) {
1!
1264
                    this.addDiagnostic({
1✔
1265
                        ...DiagnosticMessages.xmlFunctionNotFound(onChange),
1266
                        location: field.getAttribute('onchange')?.tokens.value.location
3!
1267
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1268
                }
1269
            }
1270
        }
1271
    }
1272

1273
    private validateDocComments(node: AstNode) {
1274
        const doc = brsDocParser.parseNode(node);
243✔
1275
        for (const docTag of doc.tags) {
243✔
1276
            const docTypeTag = docTag as BrsDocWithType;
29✔
1277
            if (!docTypeTag.typeExpression || !docTypeTag.location) {
29✔
1278
                continue;
1✔
1279
            }
1280
            const foundType = docTypeTag.typeExpression?.getType({ flags: SymbolTypeFlag.typetime });
28!
1281
            if (!foundType?.isResolvable()) {
28!
1282
                this.addMultiScopeDiagnostic({
8✔
1283
                    ...DiagnosticMessages.cannotFindName(docTypeTag.typeString),
1284
                    location: brsDocParser.getTypeLocationFromToken(docTypeTag.token) ?? docTypeTag.location
24!
1285
                });
1286
            }
1287
        }
1288
    }
1289

1290
    /**
1291
     * Detect when a child has imported a script that an ancestor also imported
1292
     */
1293
    private diagnosticDetectDuplicateAncestorScriptImports(scope: XmlScope) {
1294
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLImports });
478!
1295
        if (scope.xmlFile.parentComponent) {
478✔
1296
            //build a lookup of pkg paths -> FileReference so we can more easily look up collisions
1297
            let parentScriptImports = scope.xmlFile.getAncestorScriptTagImports();
34✔
1298
            let lookup = {} as Record<string, FileReference>;
34✔
1299
            for (let parentScriptImport of parentScriptImports) {
34✔
1300
                //keep the first occurance of a pkgPath. Parent imports are first in the array
1301
                if (!lookup[parentScriptImport.destPath]) {
30!
1302
                    lookup[parentScriptImport.destPath] = parentScriptImport;
30✔
1303
                }
1304
            }
1305

1306
            //add warning for every script tag that this file shares with an ancestor
1307
            for (let scriptImport of scope.xmlFile.scriptTagImports) {
34✔
1308
                let ancestorScriptImport = lookup[scriptImport.destPath];
30✔
1309
                if (ancestorScriptImport) {
30✔
1310
                    let ancestorComponent = ancestorScriptImport.sourceFile as XmlFile;
21✔
1311
                    let ancestorComponentName = ancestorComponent.componentName?.text ?? ancestorComponent.destPath;
21!
1312
                    this.addDiagnostic({
21✔
1313
                        location: util.createLocationFromFileRange(scope.xmlFile, scriptImport.filePathRange),
1314
                        ...DiagnosticMessages.unnecessaryScriptImportInChildFromParent(ancestorComponentName)
1315
                    }, ScopeValidatorDiagnosticTag.XMLImports);
1316
                }
1317
            }
1318
        }
1319
    }
1320

1321
    /**
1322
     * Wraps the AstNode.getType() method, so that we can do extra processing on the result based on the current file
1323
     * In particular, since BrightScript does not support Unions, and there's no way to cast them to something else
1324
     * if the result of .getType() is a union, and we're in a .brs (brightScript) file, treat the result as Dynamic
1325
     *
1326
     * Also, for BrightScript parse-mode, if .getType() returns a node type, do not validate unknown members.
1327
     *
1328
     * In most cases, this returns the result of node.getType()
1329
     *
1330
     * @param file the current file being processed
1331
     * @param node the node to get the type of
1332
     * @param getTypeOpts any options to pass to node.getType()
1333
     * @returns the processed result type
1334
     */
1335
    private getNodeTypeWrapper(file: BrsFile, node: AstNode, getTypeOpts: GetTypeOptions) {
1336
        const type = node?.getType(getTypeOpts);
9,082!
1337

1338
        if (file.parseMode === ParseMode.BrightScript) {
9,082✔
1339
            // this is a brightscript file
1340
            const typeChain = getTypeOpts.typeChain;
964✔
1341
            if (typeChain) {
964✔
1342
                const hasUnion = typeChain.reduce((hasUnion, tce) => {
322✔
1343
                    return hasUnion || isUnionType(tce.type);
374✔
1344
                }, false);
1345
                if (hasUnion) {
322✔
1346
                    // there was a union somewhere in the typechain
1347
                    return DynamicType.instance;
6✔
1348
                }
1349
            }
1350
            if (isUnionType(type)) {
958✔
1351
                //this is a union
1352
                return DynamicType.instance;
4✔
1353
            }
1354

1355
            if (isComponentType(type)) {
954✔
1356
                // modify type to allow any member access for Node types
1357
                type.changeUnknownMemberToDynamic = true;
18✔
1358
            }
1359
        }
1360

1361
        // by default return the result of node.getType()
1362
        return type;
9,072✔
1363
    }
1364

1365
    private getParentTypeDescriptor(typeChainResult: TypeChainProcessResult) {
1366
        if (typeChainResult.itemParentTypeKind === BscTypeKind.NamespaceType) {
236✔
1367
            return 'namespace';
117✔
1368
        }
1369
        return 'type';
119✔
1370
    }
1371

1372
    private addDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1373
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
51!
1374
        this.event.program.diagnostics.register(diagnostic, {
51✔
1375
            tags: [diagnosticTag],
1376
            segment: this.currentSegmentBeingValidated
1377
        });
1378
    }
1379

1380
    /**
1381
     * Add a diagnostic (to the first scope) that will have `relatedInformation` for each affected scope
1382
     */
1383
    private addMultiScopeDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1384
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
489✔
1385
        this.event.program.diagnostics.register(diagnostic, {
489✔
1386
            tags: [diagnosticTag],
1387
            segment: this.currentSegmentBeingValidated,
1388
            scope: this.event.scope
1389
        });
1390
    }
1391
}
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