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

rokucommunity / brighterscript / #13117

01 Oct 2024 08:24AM UTC coverage: 86.842% (-1.4%) from 88.193%
#13117

push

web-flow
Merge abd960cd5 into 3a2dc7282

11537 of 14048 branches covered (82.13%)

Branch coverage included in aggregate %.

6991 of 7582 new or added lines in 100 files covered. (92.21%)

83 existing lines in 18 files now uncovered.

12692 of 13852 relevant lines covered (91.63%)

29478.96 hits per line

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

91.36
/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, isStringType, isTypedFunctionType, isUnionType, isVariableExpression, 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 } from '../../parser/Expression';
17
import { CallExpression } from '../../parser/Expression';
1✔
18
import { createVisitor } 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';
1✔
30
import { BscTypeKind } from '../../types/BscTypeKind';
1✔
31
import type { BrsDocWithType } from '../../parser/BrightScriptDocParser';
32
import brsDocParser from '../../parser/BrightScriptDocParser';
1✔
33

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

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

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

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

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

67

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

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

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

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

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

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

110
            if (thisFileHasChanges || this.doesFileProvideChangedSymbol(file, this.event.changedSymbols)) {
1,979✔
111
                this.diagnosticDetectFunctionCollisions(file);
1,836✔
112
            }
113
        });
114

115
        this.event.scope.enumerateOwnFiles((file) => {
1,600✔
116
            if (isBrsFile(file)) {
2,398✔
117

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

121
                const hasUnvalidatedSegments = file.validationSegmenter.hasUnvalidatedSegments();
1,969✔
122

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

127
                const validationVisitor = createVisitor({
1,568✔
128
                    VariableExpression: (varExpr) => {
129
                        this.validateVariableAndDottedGetExpressions(file, varExpr);
3,389✔
130
                    },
131
                    DottedGetExpression: (dottedGet) => {
132
                        this.validateVariableAndDottedGetExpressions(file, dottedGet);
1,412✔
133
                    },
134
                    CallExpression: (functionCall) => {
135
                        this.validateFunctionCall(file, functionCall);
856✔
136
                        this.validateCreateObjectCall(file, functionCall);
856✔
137
                    },
138
                    ReturnStatement: (returnStatement) => {
139
                        this.validateReturnStatement(file, returnStatement);
261✔
140
                    },
141
                    DottedSetStatement: (dottedSetStmt) => {
142
                        this.validateDottedSetStatement(file, dottedSetStmt);
84✔
143
                    },
144
                    BinaryExpression: (binaryExpr) => {
145
                        this.validateBinaryExpression(file, binaryExpr);
230✔
146
                    },
147
                    UnaryExpression: (unaryExpr) => {
148
                        this.validateUnaryExpression(file, unaryExpr);
33✔
149
                    },
150
                    AssignmentStatement: (assignStmt) => {
151
                        this.validateAssignmentStatement(file, assignStmt);
600✔
152
                        // Note: this also includes For statements
153
                        this.detectShadowedLocalVar(file, {
600✔
154
                            expr: assignStmt,
155
                            name: assignStmt.tokens.name.text,
156
                            type: this.getNodeTypeWrapper(file, assignStmt, { flags: SymbolTypeFlag.runtime }),
157
                            nameRange: assignStmt.tokens.name.location?.range
1,800✔
158
                        });
159
                    },
160
                    AugmentedAssignmentStatement: (binaryExpr) => {
161
                        this.validateBinaryExpression(file, binaryExpr);
48✔
162
                    },
163
                    IncrementStatement: (stmt) => {
164
                        this.validateIncrementStatement(file, stmt);
9✔
165
                    },
166
                    NewExpression: (newExpr) => {
167
                        this.validateNewExpression(file, newExpr);
114✔
168
                    },
169
                    ForEachStatement: (forEachStmt) => {
170
                        this.detectShadowedLocalVar(file, {
24✔
171
                            expr: forEachStmt,
172
                            name: forEachStmt.tokens.item.text,
173
                            type: this.getNodeTypeWrapper(file, forEachStmt, { flags: SymbolTypeFlag.runtime }),
174
                            nameRange: forEachStmt.tokens.item.location?.range
72✔
175
                        });
176
                    },
177
                    FunctionParameterExpression: (funcParam) => {
178
                        this.detectShadowedLocalVar(file, {
1,038✔
179
                            expr: funcParam,
180
                            name: funcParam.tokens.name.text,
181
                            type: this.getNodeTypeWrapper(file, funcParam, { flags: SymbolTypeFlag.runtime }),
182
                            nameRange: funcParam.tokens.name.location?.range
3,114✔
183
                        });
184
                    },
185
                    AstNode: (node) => {
186
                        //check for doc comments
187
                        if (!node.leadingTrivia || node.leadingTrivia.filter(triviaToken => triviaToken.kind === TokenKind.Comment).length === 0) {
22,560✔
188
                            return;
17,339✔
189
                        }
190

191
                        this.validateDocComments(node);
228✔
192

193
                    }
194
                });
195
                // validate only what's needed in the file
196

197
                const segmentsToWalkForValidation = thisFileHasChanges
1,568✔
198
                    ? file.validationSegmenter.getAllUnvalidatedSegments()
1,568✔
199
                    : file.validationSegmenter.getSegmentsWithChangedSymbols(this.event.changedSymbols);
200

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

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

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

237
    private currentSegmentBeingValidated: AstNode;
238

239

240
    private isTypeKnown(exprType: BscType) {
241
        let isKnownType = exprType?.isResolvable();
3,489✔
242
        return isKnownType;
3,489✔
243
    }
244

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

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

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

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

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

339
    }
340

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

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

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

388
                if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) {
509✔
389
                    argType = data.definingNode.getConstructorType();
2✔
390
                }
391

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

403
        }
404
    }
405

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

413

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

425
            if (funcType.returnType.isResolvable() && actualReturnType && !funcType.returnType.isTypeCompatible(actualReturnType, compatibilityData)) {
261✔
426
                this.addMultiScopeDiagnostic({
12✔
427
                    ...DiagnosticMessages.returnTypeMismatch(actualReturnType.toString(), funcType.returnType.toString(), compatibilityData),
428
                    location: returnStmt.value.location
429
                });
430
            }
431
        }
432
    }
433

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

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

459
        let accessibilityIsOk = this.checkMemberAccessibility(file, dottedSetStmt, typeChainExpectedLHS);
40✔
460

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

470
        if (accessibilityIsOk && !expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
35!
471
            this.addMultiScopeDiagnostic({
7✔
472
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
473
                location: dottedSetStmt.location
474
            });
475
        }
476
    }
477

478
    /**
479
     * Detect when declared type does not match rhs type
480
     */
481
    private validateAssignmentStatement(file: BrsFile, assignStmt: AssignmentStatement) {
482
        if (!assignStmt?.typeExpression) {
600!
483
            // nothing to check
484
            return;
593✔
485
        }
486

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

502
    /**
503
     * Detect invalid use of a binary operator
504
     */
505
    private validateBinaryExpression(file: BrsFile, binaryExpr: BinaryExpression | AugmentedAssignmentStatement) {
506
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
278✔
507

508
        if (util.isInTypeExpression(binaryExpr)) {
278✔
509
            return;
13✔
510
        }
511

512
        let leftType = isBinaryExpression(binaryExpr)
265✔
513
            ? this.getNodeTypeWrapper(file, binaryExpr.left, getTypeOpts)
265✔
514
            : this.getNodeTypeWrapper(file, binaryExpr.item, getTypeOpts);
515
        let rightType = isBinaryExpression(binaryExpr)
265✔
516
            ? this.getNodeTypeWrapper(file, binaryExpr.right, getTypeOpts)
265✔
517
            : this.getNodeTypeWrapper(file, binaryExpr.value, getTypeOpts);
518

519
        if (!leftType.isResolvable() || !rightType.isResolvable()) {
265✔
520
            // Can not find the type. error handled elsewhere
521
            return;
12✔
522
        }
523
        let leftTypeToTest = leftType;
253✔
524
        let rightTypeToTest = rightType;
253✔
525

526
        if (isEnumMemberType(leftType) || isEnumType(leftType)) {
253✔
527
            leftTypeToTest = leftType.underlyingType;
11✔
528
        }
529
        if (isEnumMemberType(rightType) || isEnumType(rightType)) {
253✔
530
            rightTypeToTest = rightType.underlyingType;
10✔
531
        }
532

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

543

544
        if (leftIsAny && rightIsAny) {
253✔
545
            // both operands are basically "any" type... ignore;
546
            return;
19✔
547
        } else if ((leftIsAny && rightIsPrimitive) || (leftIsPrimitive && rightIsAny)) {
234✔
548
            // one operand is basically "any" type... ignore;
549
            return;
43✔
550
        }
551
        const opResult = util.binaryOperatorResultType(leftTypeToTest, binaryExpr.tokens.operator, rightTypeToTest);
191✔
552

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

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

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

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

579

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

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

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

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

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

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

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

630

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

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

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

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

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

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

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

724
            }
725
        }
726
        if (isUsedAsType) {
3,489✔
727
            return;
1,104✔
728
        }
729

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

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

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

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

749
        this.checkMemberAccessibility(file, expression, typeChain);
2,385✔
750

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

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

806
        return classUsedAsVar;
2,271✔
807
    }
808

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

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

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

857
            }
858
        }
859
        return true;
2,416✔
860
    }
861

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

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

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

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

885
        }
886
    }
887

888
    /**
889
     * Create diagnostics for any duplicate function declarations
890
     */
891
    private flagDuplicateFunctionDeclarations() {
892
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration });
1,600✔
893

894
        //for each list of callables with the same name
895
        for (let [lowerName, callableContainers] of this.event.scope.getCallableContainerMap()) {
1,600✔
896

897
            let globalCallables = [] as CallableContainer[];
120,126✔
898
            let nonGlobalCallables = [] as CallableContainer[];
120,126✔
899
            let ownCallables = [] as CallableContainer[];
120,126✔
900
            let ancestorNonGlobalCallables = [] as CallableContainer[];
120,126✔
901

902

903
            for (let container of callableContainers) {
120,126✔
904
                if (container.scope === this.event.program.globalScope) {
126,560✔
905
                    globalCallables.push(container);
124,800✔
906
                } else {
907
                    nonGlobalCallables.push(container);
1,760✔
908
                    if (container.scope === this.event.scope) {
1,760✔
909
                        ownCallables.push(container);
1,730✔
910
                    } else {
911
                        ancestorNonGlobalCallables.push(container);
30✔
912
                    }
913
                }
914
            }
915

916
            //add info diagnostics about child shadowing parent functions
917
            if (ownCallables.length > 0 && ancestorNonGlobalCallables.length > 0) {
120,126✔
918
                for (let container of ownCallables) {
24✔
919
                    //skip the init function (because every component will have one of those){
920
                    if (lowerName !== 'init') {
24✔
921
                        let shadowedCallable = ancestorNonGlobalCallables[ancestorNonGlobalCallables.length - 1];
23✔
922
                        if (!!shadowedCallable && shadowedCallable.callable.file === container.callable.file) {
23✔
923
                            //same file: skip redundant imports
924
                            continue;
20✔
925
                        }
926
                        this.addMultiScopeDiagnostic({
3✔
927
                            ...DiagnosticMessages.overridesAncestorFunction(
928
                                container.callable.name,
929
                                container.scope.name,
930
                                shadowedCallable.callable.file.destPath,
931
                                //grab the last item in the list, which should be the closest ancestor's version
932
                                shadowedCallable.scope.name
933
                            ),
934
                            location: util.createLocationFromFileRange(container.callable.file, container.callable.nameRange)
935
                        }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
936
                    }
937
                }
938
            }
939

940
            //add error diagnostics about duplicate functions in the same scope
941
            if (ownCallables.length > 1) {
120,126✔
942

943
                for (let callableContainer of ownCallables) {
5✔
944
                    let callable = callableContainer.callable;
10✔
945
                    const related = [];
10✔
946
                    for (const ownCallable of ownCallables) {
10✔
947
                        const thatNameRange = ownCallable.callable.nameRange;
20✔
948
                        if (ownCallable.callable.nameRange !== callable.nameRange) {
20✔
949
                            related.push({
10✔
950
                                message: `Function declared here`,
951
                                location: util.createLocationFromRange(
952
                                    util.pathToUri(ownCallable.callable.file?.srcPath),
30!
953
                                    thatNameRange
954
                                )
955
                            });
956
                        }
957
                    }
958

959
                    this.addMultiScopeDiagnostic({
10✔
960
                        ...DiagnosticMessages.duplicateFunctionImplementation(callable.name),
961
                        location: util.createLocationFromFileRange(callable.file, callable.nameRange),
962
                        relatedInformation: related
963
                    }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
964
                }
965
            }
966
        }
967
    }
968

969
    /**
970
     * Verify that all of the scripts imported by each file in this scope actually exist, and have the correct case
971
     */
972
    private validateScriptImportPaths() {
973
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Imports });
1,600✔
974

975
        let scriptImports = this.event.scope.getOwnScriptImports();
1,600✔
976
        //verify every script import
977
        for (let scriptImport of scriptImports) {
1,600✔
978
            let referencedFile = this.event.scope.getFileByRelativePath(scriptImport.destPath);
496✔
979
            //if we can't find the file
980
            if (!referencedFile) {
496✔
981
                //skip the default bslib file, it will exist at transpile time but should not show up in the program during validation cycle
982
                if (scriptImport.destPath === this.event.program.bslibPkgPath) {
17✔
983
                    continue;
2✔
984
                }
985
                let dInfo: DiagnosticInfo;
986
                if (scriptImport.text.trim().length === 0) {
15✔
987
                    dInfo = DiagnosticMessages.scriptSrcCannotBeEmpty();
1✔
988
                } else {
989
                    dInfo = DiagnosticMessages.referencedFileDoesNotExist();
14✔
990
                }
991

992
                this.addMultiScopeDiagnostic({
15✔
993
                    ...dInfo,
994
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
995
                }, ScopeValidatorDiagnosticTag.Imports);
996
                //if the character casing of the script import path does not match that of the actual path
997
            } else if (scriptImport.destPath !== referencedFile.destPath) {
479✔
998
                this.addMultiScopeDiagnostic({
2✔
999
                    ...DiagnosticMessages.scriptImportCaseMismatch(referencedFile.destPath),
1000
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
1001
                }, ScopeValidatorDiagnosticTag.Imports);
1002
            }
1003
        }
1004
    }
1005

1006
    /**
1007
     * Validate all classes defined in this scope
1008
     */
1009
    private validateClasses() {
1010
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Classes });
1,600✔
1011

1012
        let validator = new BsClassValidator(this.event.scope);
1,600✔
1013
        validator.validate();
1,600✔
1014
        for (const diagnostic of validator.diagnostics) {
1,600✔
1015
            this.addMultiScopeDiagnostic({
29✔
1016
                ...diagnostic
1017
            }, ScopeValidatorDiagnosticTag.Classes);
1018
        }
1019
    }
1020

1021

1022
    /**
1023
     * Find various function collisions
1024
     */
1025
    private diagnosticDetectFunctionCollisions(file: BrsFile) {
1026
        const fileUri = util.pathToUri(file.srcPath);
1,836✔
1027
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, tag: ScopeValidatorDiagnosticTag.FunctionCollisions });
1,836✔
1028
        for (let func of file.callables) {
1,836✔
1029
            const funcName = func.getName(ParseMode.BrighterScript);
1,599✔
1030
            const lowerFuncName = funcName?.toLowerCase();
1,599!
1031
            if (lowerFuncName) {
1,599!
1032

1033
                //find function declarations with the same name as a stdlib function
1034
                if (globalCallableMap.has(lowerFuncName)) {
1,599✔
1035
                    this.addMultiScopeDiagnostic({
5✔
1036
                        ...DiagnosticMessages.scopeFunctionShadowedByBuiltInFunction(),
1037
                        location: util.createLocationFromRange(fileUri, func.nameRange)
1038

1039
                    });
1040
                }
1041
            }
1042
        }
1043
    }
1044

1045
    public detectShadowedLocalVar(file: BrsFile, varDeclaration: { expr: AstNode; name: string; type: BscType; nameRange: Range }) {
1046
        const varName = varDeclaration.name;
1,662✔
1047
        const lowerVarName = varName.toLowerCase();
1,662✔
1048
        const callableContainerMap = this.event.scope.getCallableContainerMap();
1,662✔
1049
        const containingNamespace = varDeclaration.expr?.findAncestor<NamespaceStatement>(isNamespaceStatement);
1,662!
1050
        const localVarIsInNamespace = util.isVariableMemberOfNamespace(varDeclaration.name, varDeclaration.expr, containingNamespace);
1,662✔
1051

1052
        const varIsFunction = () => {
1,662✔
1053
            return isCallableType(varDeclaration.type);
11✔
1054
        };
1055

1056
        if (
1,662✔
1057
            //has same name as stdlib
1058
            globalCallableMap.has(lowerVarName)
1059
        ) {
1060
            //local var function with same name as stdlib function
1061
            if (varIsFunction()) {
8✔
1062
                this.addMultiScopeDiagnostic({
1✔
1063
                    ...DiagnosticMessages.localVarFunctionShadowsParentFunction('stdlib'),
1064
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange)
1065
                });
1066
            }
1067
        } else if (callableContainerMap.has(lowerVarName) && !localVarIsInNamespace) {
1,654✔
1068
            const callable = callableContainerMap.get(lowerVarName);
3✔
1069
            //is same name as a callable
1070
            if (varIsFunction()) {
3✔
1071
                this.addMultiScopeDiagnostic({
1✔
1072
                    ...DiagnosticMessages.localVarFunctionShadowsParentFunction('scope'),
1073
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1074
                    relatedInformation: [{
1075
                        message: 'Function declared here',
1076
                        location: util.createLocationFromFileRange(
1077
                            callable[0].callable.file,
1078
                            callable[0].callable.nameRange
1079
                        )
1080
                    }]
1081
                });
1082
            } else {
1083
                this.addMultiScopeDiagnostic({
2✔
1084
                    ...DiagnosticMessages.localVarShadowedByScopedFunction(),
1085
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1086
                    relatedInformation: [{
1087
                        message: 'Function declared here',
1088
                        location: util.createLocationFromRange(
1089
                            util.pathToUri(callable[0].callable.file.srcPath),
1090
                            callable[0].callable.nameRange
1091
                        )
1092
                    }]
1093
                });
1094
            }
1095
            //has the same name as an in-scope class
1096
        } else if (!localVarIsInNamespace) {
1,651✔
1097
            const classStmtLink = this.event.scope.getClassFileLink(lowerVarName);
1,501✔
1098
            if (classStmtLink) {
1,501✔
1099
                this.addMultiScopeDiagnostic({
3✔
1100
                    ...DiagnosticMessages.localVarSameNameAsClass(classStmtLink?.item?.getName(ParseMode.BrighterScript)),
18!
1101
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1102
                    relatedInformation: [{
1103
                        message: 'Class declared here',
1104
                        location: util.createLocationFromRange(
1105
                            util.pathToUri(classStmtLink.file.srcPath),
1106
                            classStmtLink?.item.tokens.name.location?.range
18!
1107
                        )
1108
                    }]
1109
                });
1110
            }
1111
        }
1112
    }
1113

1114
    private validateXmlInterface(scope: XmlScope) {
1115
        if (!scope.xmlFile.parser.ast?.componentElement?.interfaceElement) {
429!
1116
            return;
398✔
1117
        }
1118
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLInterface });
31!
1119

1120
        const iface = scope.xmlFile.parser.ast.componentElement.interfaceElement;
31✔
1121
        const callableContainerMap = scope.getCallableContainerMap();
31✔
1122
        //validate functions
1123
        for (const func of iface.functions) {
31✔
1124
            const name = func.name;
33✔
1125
            if (!name) {
33✔
1126
                this.addDiagnostic({
3✔
1127
                    ...DiagnosticMessages.xmlTagMissingAttribute(func.tokens.startTagName.text, 'name'),
1128
                    location: func.tokens.startTagName.location
1129
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1130
            } else if (!callableContainerMap.has(name.toLowerCase())) {
30✔
1131
                this.addDiagnostic({
4✔
1132
                    ...DiagnosticMessages.xmlFunctionNotFound(name),
1133
                    location: func.getAttribute('name')?.tokens.value.location
12!
1134
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1135
            }
1136
        }
1137
        //validate fields
1138
        for (const field of iface.fields) {
31✔
1139
            const { id, type, onChange } = field;
28✔
1140
            if (!id) {
28✔
1141
                this.addDiagnostic({
3✔
1142
                    ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'id'),
1143
                    location: field.tokens.startTagName.location
1144
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1145
            }
1146
            if (!type) {
28✔
1147
                if (!field.alias) {
3✔
1148
                    this.addDiagnostic({
2✔
1149
                        ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'type'),
1150
                        location: field.tokens.startTagName.location
1151
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1152
                }
1153
            } else if (!SGFieldTypes.includes(type.toLowerCase())) {
25✔
1154
                this.addDiagnostic({
1✔
1155
                    ...DiagnosticMessages.xmlInvalidFieldType(type),
1156
                    location: field.getAttribute('type')?.tokens.value.location
3!
1157
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1158
            }
1159
            if (onChange) {
28✔
1160
                if (!callableContainerMap.has(onChange.toLowerCase())) {
1!
1161
                    this.addDiagnostic({
1✔
1162
                        ...DiagnosticMessages.xmlFunctionNotFound(onChange),
1163
                        location: field.getAttribute('onchange')?.tokens.value.location
3!
1164
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1165
                }
1166
            }
1167
        }
1168
    }
1169

1170
    private validateDocComments(node: AstNode) {
1171
        const doc = brsDocParser.parseNode(node);
228✔
1172
        for (const docTag of doc.tags) {
228✔
1173
            const docTypeTag = docTag as BrsDocWithType;
24✔
1174
            if (!docTypeTag.typeExpression || !docTypeTag.location) {
24✔
1175
                continue;
1✔
1176
            }
1177
            const foundType = docTypeTag.typeExpression?.getType({ flags: SymbolTypeFlag.typetime });
23!
1178
            if (!foundType?.isResolvable()) {
23!
1179
                this.addMultiScopeDiagnostic({
8✔
1180
                    ...DiagnosticMessages.cannotFindTypeInCommentDoc(docTypeTag.typeString),
1181
                    location: brsDocParser.getTypeLocationFromToken(docTypeTag.token) ?? docTypeTag.location
24!
1182
                });
1183
            }
1184
        }
1185
    }
1186

1187
    /**
1188
     * Detect when a child has imported a script that an ancestor also imported
1189
     */
1190
    private diagnosticDetectDuplicateAncestorScriptImports(scope: XmlScope) {
1191
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLImports });
429!
1192
        if (scope.xmlFile.parentComponent) {
429✔
1193
            //build a lookup of pkg paths -> FileReference so we can more easily look up collisions
1194
            let parentScriptImports = scope.xmlFile.getAncestorScriptTagImports();
34✔
1195
            let lookup = {} as Record<string, FileReference>;
34✔
1196
            for (let parentScriptImport of parentScriptImports) {
34✔
1197
                //keep the first occurance of a pkgPath. Parent imports are first in the array
1198
                if (!lookup[parentScriptImport.destPath]) {
30!
1199
                    lookup[parentScriptImport.destPath] = parentScriptImport;
30✔
1200
                }
1201
            }
1202

1203
            //add warning for every script tag that this file shares with an ancestor
1204
            for (let scriptImport of scope.xmlFile.scriptTagImports) {
34✔
1205
                let ancestorScriptImport = lookup[scriptImport.destPath];
30✔
1206
                if (ancestorScriptImport) {
30✔
1207
                    let ancestorComponent = ancestorScriptImport.sourceFile as XmlFile;
21✔
1208
                    let ancestorComponentName = ancestorComponent.componentName?.text ?? ancestorComponent.destPath;
21!
1209
                    this.addDiagnostic({
21✔
1210
                        location: util.createLocationFromFileRange(scope.xmlFile, scriptImport.filePathRange),
1211
                        ...DiagnosticMessages.unnecessaryScriptImportInChildFromParent(ancestorComponentName)
1212
                    }, ScopeValidatorDiagnosticTag.XMLImports);
1213
                }
1214
            }
1215
        }
1216
    }
1217

1218
    /**
1219
     * Wraps the AstNode.getType() method, so that we can do extra processing on the result based on the current file
1220
     * In particular, since BrightScript does not support Unions, and there's no way to cast them to something else
1221
     * if the result of .getType() is a union, and we're in a .brs (brightScript) file, treat the result as Dynamic
1222
     *
1223
     * In most cases, this returns the result of node.getType()
1224
     *
1225
     * @param file the current file being processed
1226
     * @param node the node to get the type of
1227
     * @param getTypeOpts any options to pass to node.getType()
1228
     * @returns the processed result type
1229
     */
1230
    private getNodeTypeWrapper(file: BrsFile, node: AstNode, getTypeOpts: GetTypeOptions) {
1231
        const type = node?.getType(getTypeOpts);
7,895✔
1232

1233
        if (file.parseMode === ParseMode.BrightScript) {
7,895✔
1234
            // this is a brightscript file
1235
            const typeChain = getTypeOpts.typeChain;
948✔
1236
            if (typeChain) {
948✔
1237
                const hasUnion = typeChain.reduce((hasUnion, tce) => {
328✔
1238
                    return hasUnion || isUnionType(tce.type);
379✔
1239
                }, false);
1240
                if (hasUnion) {
328✔
1241
                    // there was a union somewhere in the typechain
1242
                    return DynamicType.instance;
6✔
1243
                }
1244
            }
1245
            if (isUnionType(type)) {
942✔
1246
                //this is a union
1247
                return DynamicType.instance;
4✔
1248
            }
1249
        }
1250

1251
        // by default return the result of node.getType()
1252
        return type;
7,885✔
1253
    }
1254

1255
    private getParentTypeDescriptor(typeChainResult: TypeChainProcessResult) {
1256
        if (typeChainResult.itemParentTypeKind === BscTypeKind.NamespaceType) {
231✔
1257
            return 'namespace';
117✔
1258
        }
1259
        return 'type';
114✔
1260
    }
1261

1262
    private addDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1263
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
51!
1264
        this.event.program.diagnostics.register(diagnostic, {
51✔
1265
            tags: [diagnosticTag],
1266
            segment: this.currentSegmentBeingValidated
1267
        });
1268
    }
1269

1270
    /**
1271
     * Add a diagnostic (to the first scope) that will have `relatedInformation` for each affected scope
1272
     */
1273
    private addMultiScopeDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1274
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
452✔
1275
        this.event.program.diagnostics.register(diagnostic, {
452✔
1276
            tags: [diagnosticTag],
1277
            segment: this.currentSegmentBeingValidated,
1278
            scope: this.event.scope
1279
        });
1280
    }
1281
}
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