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

rokucommunity / brighterscript / #13387

29 Nov 2024 07:55PM UTC coverage: 86.824% (-0.004%) from 86.828%
#13387

push

web-flow
Merge b1d7d7253 into 57fa2ad4d

12087 of 14723 branches covered (82.1%)

Branch coverage included in aggregate %.

379 of 407 new or added lines in 36 files covered. (93.12%)

244 existing lines in 22 files now uncovered.

13071 of 14253 relevant lines covered (91.71%)

33086.62 hits per line

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

91.37
/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, 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 { VoidType } from '../../types/VoidType';
1✔
31
import { BscTypeKind } from '../../types/BscTypeKind';
1✔
32
import type { BrsDocWithType } from '../../parser/BrightScriptDocParser';
33
import brsDocParser from '../../parser/BrightScriptDocParser';
1✔
34
import type { Location } from 'vscode-languageserver';
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,820✔
68

69

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

235
            for (let providedKey of providedSymbolKeysFlag) {
304✔
236
                if (changedSymbolSetForFlag.has(providedKey)) {
282✔
237
                    return true;
7✔
238
                }
239
            }
240
        }
241
        return false;
145✔
242
    }
243

244
    private currentSegmentBeingValidated: AstNode;
245

246

247
    private isTypeKnown(exprType: BscType) {
248
        let isKnownType = exprType?.isResolvable();
3,724✔
249
        return isKnownType;
3,724✔
250
    }
251

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

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

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

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

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

336
    }
337

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

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

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

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

389

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

400
    }
401

402
    private validateCallFuncExpression(file: BrsFile, expression: CallfuncExpression) {
403
        const callerType = expression.callee?.getType({ flags: SymbolTypeFlag.runtime });
34!
404
        if (isDynamicType(callerType)) {
34✔
405
            return;
17✔
406
        }
407
        const methodToken = expression.tokens.methodName;
17✔
408
        const methodName = methodToken?.text ?? '';
17✔
409
        const functionFullname = `${callerType.toString()}@.${methodName}`;
17✔
410
        const callErrorLocation = expression.location;
17✔
411

412
        if (!isComponentType(callerType)) {
17✔
413
            this.addMultiScopeDiagnostic({
1✔
414
                ...DiagnosticMessages.cannotFindCallFuncFunction(methodName, functionFullname, callerType.toString()),
415
                location: callErrorLocation
416
            });
417
            return;
1✔
418
        }
419
        if (util.isGenericNodeType(callerType)) {
16✔
420
            // ignore "general" node
421
            return;
1✔
422
        }
423
        const funcType = util.getCallFuncType(expression, methodToken, { flags: SymbolTypeFlag.runtime, ignoreCall: true });
15✔
424
        if (!funcType?.isResolvable()) {
12✔
425
            this.addMultiScopeDiagnostic({
1✔
426
                ...DiagnosticMessages.cannotFindCallFuncFunction(methodName, functionFullname, callerType.toString()),
427
                location: callErrorLocation
428
            });
429
        }
430
        return this.validateFunctionCall(file, funcType, callErrorLocation, expression.args);
12✔
431
    }
432

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

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

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

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

477
            if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) {
555✔
478
                argType = data.definingNode.getConstructorType();
2✔
479
            }
480

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

493
    private checkAllowedArgConversions(paramType: BscType, argType: BscType): boolean {
494
        if (isNumberType(argType) && isBooleanType(paramType)) {
555✔
495
            return true;
8✔
496
        }
497
        return false;
547✔
498
    }
499

500

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

514
            if (funcType.returnType.isResolvable() && actualReturnType && !funcType.returnType.isTypeCompatible(actualReturnType, compatibilityData)) {
338✔
515
                this.addMultiScopeDiagnostic({
14✔
516
                    ...DiagnosticMessages.returnTypeMismatch(actualReturnType.toString(), funcType.returnType.toString(), compatibilityData),
517
                    location: returnStmt.value?.location ?? returnStmt.location
84✔
518
                });
519
            }
520
        }
521
    }
522

523
    /**
524
     * Detect assigned type different from expected member type
525
     */
526
    private validateDottedSetStatement(file: BrsFile, dottedSetStmt: DottedSetStatement) {
527
        const typeChainExpectedLHS = [] as TypeChainEntry[];
95✔
528
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
95✔
529

530
        const expectedLHSType = this.getNodeTypeWrapper(file, dottedSetStmt, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
95✔
531
        const actualRHSType = this.getNodeTypeWrapper(file, dottedSetStmt?.value, getTypeOpts);
95!
532
        const compatibilityData: TypeCompatibilityData = {};
95✔
533
        const typeChainScan = util.processTypeChain(typeChainExpectedLHS);
95✔
534
        // check if anything in typeChain is an AA - if so, just allow it
535
        if (typeChainExpectedLHS.find(typeChainItem => isAssociativeArrayType(typeChainItem.type))) {
163✔
536
            // something in the chain is an AA
537
            // treat members as dynamic - they could have been set without the type system's knowledge
538
            return;
39✔
539
        }
540
        if (!expectedLHSType || !expectedLHSType?.isResolvable()) {
56!
541
            this.addMultiScopeDiagnostic({
5✔
542
                ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
543
                location: typeChainScan?.location
15!
544
            });
545
            return;
5✔
546
        }
547

548
        let accessibilityIsOk = this.checkMemberAccessibility(file, dottedSetStmt, typeChainExpectedLHS);
51✔
549

550
        //Most Component fields can be set with strings
551
        //TODO: be more precise about which fields can actually accept strings
552
        //TODO: if RHS is a string literal, we can do more validation to make sure it's the correct type
553
        if (isComponentType(dottedSetStmt.obj?.getType({ flags: SymbolTypeFlag.runtime }))) {
51!
554
            if (isStringType(actualRHSType)) {
19✔
555
                return;
6✔
556
            }
557
        }
558

559
        if (accessibilityIsOk && !expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
45!
560
            this.addMultiScopeDiagnostic({
10✔
561
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
562
                location: dottedSetStmt.location
563
            });
564
        }
565
    }
566

567
    /**
568
     * Detect when declared type does not match rhs type
569
     */
570
    private validateAssignmentStatement(file: BrsFile, assignStmt: AssignmentStatement) {
571
        if (!assignStmt?.typeExpression) {
630!
572
            // nothing to check
573
            return;
623✔
574
        }
575

576
        const typeChainExpectedLHS = [];
7✔
577
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
7✔
578
        const expectedLHSType = this.getNodeTypeWrapper(file, assignStmt.typeExpression, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
7✔
579
        const actualRHSType = this.getNodeTypeWrapper(file, assignStmt.value, getTypeOpts);
7✔
580
        const compatibilityData: TypeCompatibilityData = {};
7✔
581
        if (!expectedLHSType || !expectedLHSType.isResolvable()) {
7✔
582
            // LHS is not resolvable... handled elsewhere
583
        } else if (!expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
6!
584
            this.addMultiScopeDiagnostic({
1✔
585
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
586
                location: assignStmt.location
587
            });
588
        }
589
    }
590

591
    /**
592
     * Detect invalid use of a binary operator
593
     */
594
    private validateBinaryExpression(file: BrsFile, binaryExpr: BinaryExpression | AugmentedAssignmentStatement) {
595
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
293✔
596

597
        if (util.isInTypeExpression(binaryExpr)) {
293✔
598
            return;
13✔
599
        }
600

601
        let leftType = isBinaryExpression(binaryExpr)
280✔
602
            ? this.getNodeTypeWrapper(file, binaryExpr.left, getTypeOpts)
280✔
603
            : this.getNodeTypeWrapper(file, binaryExpr.item, getTypeOpts);
604
        let rightType = isBinaryExpression(binaryExpr)
280✔
605
            ? this.getNodeTypeWrapper(file, binaryExpr.right, getTypeOpts)
280✔
606
            : this.getNodeTypeWrapper(file, binaryExpr.value, getTypeOpts);
607

608
        if (!leftType.isResolvable() || !rightType.isResolvable()) {
280✔
609
            // Can not find the type. error handled elsewhere
610
            return;
12✔
611
        }
612
        let leftTypeToTest = leftType;
268✔
613
        let rightTypeToTest = rightType;
268✔
614

615
        if (isEnumMemberType(leftType) || isEnumType(leftType)) {
268✔
616
            leftTypeToTest = leftType.underlyingType;
11✔
617
        }
618
        if (isEnumMemberType(rightType) || isEnumType(rightType)) {
268✔
619
            rightTypeToTest = rightType.underlyingType;
10✔
620
        }
621

622
        if (isUnionType(leftType) || isUnionType(rightType)) {
268!
623
            // TODO: it is possible to validate based on innerTypes, but more complicated
624
            // Because you need to verify each combination of types
UNCOV
625
            return;
×
626
        }
627
        const leftIsPrimitive = isPrimitiveType(leftTypeToTest);
268✔
628
        const rightIsPrimitive = isPrimitiveType(rightTypeToTest);
268✔
629
        const leftIsAny = isDynamicType(leftTypeToTest) || isObjectType(leftTypeToTest);
268✔
630
        const rightIsAny = isDynamicType(rightTypeToTest) || isObjectType(rightTypeToTest);
268✔
631

632

633
        if (leftIsAny && rightIsAny) {
268✔
634
            // both operands are basically "any" type... ignore;
635
            return;
25✔
636
        } else if ((leftIsAny && rightIsPrimitive) || (leftIsPrimitive && rightIsAny)) {
243✔
637
            // one operand is basically "any" type... ignore;
638
            return;
47✔
639
        }
640
        const opResult = util.binaryOperatorResultType(leftTypeToTest, binaryExpr.tokens.operator, rightTypeToTest);
196✔
641

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

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

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

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

668

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

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

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

693
    private validateIncrementStatement(file: BrsFile, incStmt: IncrementStatement) {
694
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
9✔
695

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

698
        if (!rightType.isResolvable()) {
9!
699
            // Can not find the type. error handled elsewhere
UNCOV
700
            return;
×
701
        }
702

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

719

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

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

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

753
        const hasValidDeclaration = this.hasValidDeclaration(expression, exprType, typeData?.definingNode);
3,724!
754

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

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

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

813
            }
814
        }
815
        if (isUsedAsType) {
3,724✔
816
            return;
1,206✔
817
        }
818

819
        const containingNamespace = expression.findAncestor<NamespaceStatement>(isNamespaceStatement);
2,518✔
820
        const containingNamespaceName = containingNamespace?.getName(ParseMode.BrighterScript);
2,518✔
821

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

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

835
        const lastTypeInfo = typeChain[typeChain.length - 1];
2,518✔
836
        const parentTypeInfo = typeChain[typeChain.length - 2];
2,518✔
837

838
        this.checkMemberAccessibility(file, expression, typeChain);
2,518✔
839

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

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

895
        return classUsedAsVar;
2,403✔
896
    }
897

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

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

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

946
            }
947
        }
948
        return true;
2,560✔
949
    }
950

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

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

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

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

974
        }
975
    }
976

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

980
        if (!returnType || !returnType.isResolvable() || isVoidType(returnType) || isDynamicType(returnType)) {
1,986✔
981
            return;
1,773✔
982
        }
983
        const returns = func.body?.findChild<ReturnStatement>(isReturnStatement, { walkMode: WalkMode.visitAll });
213!
984
        if (!returns) {
213✔
985
            this.addMultiScopeDiagnostic({
5✔
986
                ...DiagnosticMessages.expectedReturnStatement(),
987
                location: func.location
988
            });
989
        }
990
    }
991

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

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

1001
            let globalCallables = [] as CallableContainer[];
128,002✔
1002
            let nonGlobalCallables = [] as CallableContainer[];
128,002✔
1003
            let ownCallables = [] as CallableContainer[];
128,002✔
1004
            let ancestorNonGlobalCallables = [] as CallableContainer[];
128,002✔
1005

1006

1007
            for (let container of callableContainers) {
128,002✔
1008
                if (container.scope === this.event.program.globalScope) {
134,856✔
1009
                    globalCallables.push(container);
132,990✔
1010
                } else {
1011
                    nonGlobalCallables.push(container);
1,866✔
1012
                    if (container.scope === this.event.scope) {
1,866✔
1013
                        ownCallables.push(container);
1,836✔
1014
                    } else {
1015
                        ancestorNonGlobalCallables.push(container);
30✔
1016
                    }
1017
                }
1018
            }
1019

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

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

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

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

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

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

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

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

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

1125

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1362
        // by default return the result of node.getType()
1363
        return type;
8,416✔
1364
    }
1365

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

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

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