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

rokucommunity / brighterscript / #13215

19 Oct 2024 06:15PM UTC coverage: 86.838% (-1.4%) from 88.214%
#13215

push

web-flow
Merge 65d0479eb into 7cfaaa047

11593 of 14113 branches covered (82.14%)

Branch coverage included in aggregate %.

7018 of 7610 new or added lines in 100 files covered. (92.22%)

87 existing lines in 18 files now uncovered.

12720 of 13885 relevant lines covered (91.61%)

29935.23 hits per line

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

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

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

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

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

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

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

68

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

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

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

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

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

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

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

116
        this.event.scope.enumerateOwnFiles((file) => {
1,623✔
117
            if (isBrsFile(file)) {
2,423✔
118

119
                const fileUri = util.pathToUri(file.srcPath);
1,994✔
120
                const thisFileHasChanges = this.event.changedFiles.includes(file);
1,994✔
121

122
                const hasUnvalidatedSegments = file.validationSegmenter.hasUnvalidatedSegments();
1,994✔
123

124
                if (hasChangeInfo && !hasUnvalidatedSegments) {
1,994✔
125
                    return;
401✔
126
                }
127

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

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

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

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

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

239
    private currentSegmentBeingValidated: AstNode;
240

241

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

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

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

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

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

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

341
    }
342

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

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

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

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

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

405
        }
406
    }
407

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

415

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

429
            if (funcType.returnType.isResolvable() && actualReturnType && !funcType.returnType.isTypeCompatible(actualReturnType, compatibilityData)) {
321✔
430
                this.addMultiScopeDiagnostic({
14✔
431
                    ...DiagnosticMessages.returnTypeMismatch(actualReturnType.toString(), funcType.returnType.toString(), compatibilityData),
432
                    location: returnStmt.value?.location ?? returnStmt.location
84✔
433
                });
434
            }
435
        }
436
    }
437

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

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

463
        let accessibilityIsOk = this.checkMemberAccessibility(file, dottedSetStmt, typeChainExpectedLHS);
40✔
464

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

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

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

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

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

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

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

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

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

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

547

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

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

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

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

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

583

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

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

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

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

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

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

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

634

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

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

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

668
        const hasValidDeclaration = this.hasValidDeclaration(expression, exprType, typeData?.definingNode);
3,558!
669

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

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

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

728
            }
729
        }
730
        if (isUsedAsType) {
3,558✔
731
            return;
1,128✔
732
        }
733

734
        const containingNamespace = expression.findAncestor<NamespaceStatement>(isNamespaceStatement);
2,430✔
735
        const containingNamespaceName = containingNamespace?.getName(ParseMode.BrighterScript);
2,430✔
736

737
        if (!(isCallExpression(expression.parent) && isNewExpression(expression.parent?.parent))) {
2,430!
738
            const classUsedAsVarEntry = this.checkTypeChainForClassUsedAsVar(typeChain, containingNamespaceName);
2,315✔
739
            const isClassInNamespace = containingNamespace?.getSymbolTable().hasSymbol(typeChain[0].name, SymbolTypeFlag.runtime);
2,315✔
740
            if (classUsedAsVarEntry && !isClassInNamespace) {
2,315!
741

NEW
742
                this.addMultiScopeDiagnostic({
×
743
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable(classUsedAsVarEntry.toString()),
744
                    location: expression.location
745
                });
NEW
746
                return;
×
747
            }
748
        }
749

750
        const lastTypeInfo = typeChain[typeChain.length - 1];
2,430✔
751
        const parentTypeInfo = typeChain[typeChain.length - 2];
2,430✔
752

753
        this.checkMemberAccessibility(file, expression, typeChain);
2,430✔
754

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

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

810
        return classUsedAsVar;
2,315✔
811
    }
812

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

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

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

861
            }
862
        }
863
        return true;
2,461✔
864
    }
865

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

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

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

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

889
        }
890
    }
891

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

895
        if (!returnType || !returnType.isResolvable() || isVoidType(returnType) || isDynamicType(returnType)) {
1,919✔
896
            return;
1,720✔
897
        }
898
        const returns = func.body?.findChild<ReturnStatement>(isReturnStatement, { walkMode: WalkMode.visitAll });
199!
899
        if (!returns) {
199✔
900
            this.addMultiScopeDiagnostic({
5✔
901
                ...DiagnosticMessages.expectedReturnStatement(),
902
                location: func.location
903
            });
904
        }
905
    }
906

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

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

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

921

922
            for (let container of callableContainers) {
121,868✔
923
                if (container.scope === this.event.program.globalScope) {
128,394✔
924
                    globalCallables.push(container);
126,594✔
925
                } else {
926
                    nonGlobalCallables.push(container);
1,800✔
927
                    if (container.scope === this.event.scope) {
1,800✔
928
                        ownCallables.push(container);
1,770✔
929
                    } else {
930
                        ancestorNonGlobalCallables.push(container);
30✔
931
                    }
932
                }
933
            }
934

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

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

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

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

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

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

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

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

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

1040

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

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

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

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

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

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

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

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

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

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

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

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

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

1270
        // by default return the result of node.getType()
1271
        return type;
8,071✔
1272
    }
1273

1274
    private getParentTypeDescriptor(typeChainResult: TypeChainProcessResult) {
1275
        if (typeChainResult.itemParentTypeKind === BscTypeKind.NamespaceType) {
232✔
1276
            return 'namespace';
117✔
1277
        }
1278
        return 'type';
115✔
1279
    }
1280

1281
    private addDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1282
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
51!
1283
        this.event.program.diagnostics.register(diagnostic, {
51✔
1284
            tags: [diagnosticTag],
1285
            segment: this.currentSegmentBeingValidated
1286
        });
1287
    }
1288

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