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

rokucommunity / brighterscript / #13031

05 Sep 2024 06:52PM UTC coverage: 86.418%. Remained the same
#13031

push

web-flow
Merge 0fda0c105 into 43cbf8b72

10784 of 13272 branches covered (81.25%)

Branch coverage included in aggregate %.

153 of 158 new or added lines in 7 files covered. (96.84%)

157 existing lines in 6 files now uncovered.

12484 of 13653 relevant lines covered (91.44%)

27481.68 hits per line

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

91.25
/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

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

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

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

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

63
    private metrics = new Map<string, number>();
1,587✔
64

65

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

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

93
    public reset() {
94
        this.event = undefined;
1,278✔
95
    }
96

97
    private walkFiles() {
98
        const hasChangeInfo = this.event.changedFiles && this.event.changedSymbols;
1,591✔
99

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

106
            const thisFileHasChanges = this.event.changedFiles.includes(file);
1,970✔
107

108
            if (thisFileHasChanges || this.doesFileProvideChangedSymbol(file, this.event.changedSymbols)) {
1,970✔
109
                this.diagnosticDetectFunctionCollisions(file);
1,827✔
110
            }
111
        });
112

113
        this.event.scope.enumerateOwnFiles((file) => {
1,591✔
114
            if (isBrsFile(file)) {
2,389✔
115

116
                const fileUri = util.pathToUri(file.srcPath);
1,960✔
117
                const thisFileHasChanges = this.event.changedFiles.includes(file);
1,960✔
118

119
                const hasUnvalidatedSegments = file.validationSegmenter.hasUnvalidatedSegments();
1,960✔
120

121
                if (hasChangeInfo && !hasUnvalidatedSegments) {
1,960✔
122
                    return;
401✔
123
                }
124

125
                const validationVisitor = createVisitor({
1,559✔
126
                    VariableExpression: (varExpr) => {
127
                        this.validateVariableAndDottedGetExpressions(file, varExpr);
3,378✔
128
                    },
129
                    DottedGetExpression: (dottedGet) => {
130
                        this.validateVariableAndDottedGetExpressions(file, dottedGet);
1,406✔
131
                    },
132
                    CallExpression: (functionCall) => {
133
                        this.validateFunctionCall(file, functionCall);
854✔
134
                        this.validateCreateObjectCall(file, functionCall);
854✔
135
                    },
136
                    ReturnStatement: (returnStatement) => {
137
                        this.validateReturnStatement(file, returnStatement);
260✔
138
                    },
139
                    DottedSetStatement: (dottedSetStmt) => {
140
                        this.validateDottedSetStatement(file, dottedSetStmt);
82✔
141
                    },
142
                    BinaryExpression: (binaryExpr) => {
143
                        this.validateBinaryExpression(file, binaryExpr);
229✔
144
                    },
145
                    UnaryExpression: (unaryExpr) => {
146
                        this.validateUnaryExpression(file, unaryExpr);
33✔
147
                    },
148
                    AssignmentStatement: (assignStmt) => {
149
                        this.validateAssignmentStatement(file, assignStmt);
595✔
150
                        // Note: this also includes For statements
151
                        this.detectShadowedLocalVar(file, {
595✔
152
                            expr: assignStmt,
153
                            name: assignStmt.tokens.name.text,
154
                            type: this.getNodeTypeWrapper(file, assignStmt, { flags: SymbolTypeFlag.runtime }),
155
                            nameRange: assignStmt.tokens.name.location?.range
1,785✔
156
                        });
157
                    },
158
                    AugmentedAssignmentStatement: (binaryExpr) => {
159
                        this.validateBinaryExpression(file, binaryExpr);
48✔
160
                    },
161
                    IncrementStatement: (stmt) => {
162
                        this.validateIncrementStatement(file, stmt);
9✔
163
                    },
164
                    NewExpression: (newExpr) => {
165
                        this.validateNewExpression(file, newExpr);
114✔
166
                    },
167
                    ForEachStatement: (forEachStmt) => {
168
                        this.detectShadowedLocalVar(file, {
24✔
169
                            expr: forEachStmt,
170
                            name: forEachStmt.tokens.item.text,
171
                            type: this.getNodeTypeWrapper(file, forEachStmt, { flags: SymbolTypeFlag.runtime }),
172
                            nameRange: forEachStmt.tokens.item.location?.range
72✔
173
                        });
174
                    },
175
                    FunctionParameterExpression: (funcParam) => {
176
                        this.detectShadowedLocalVar(file, {
1,034✔
177
                            expr: funcParam,
178
                            name: funcParam.tokens.name.text,
179
                            type: this.getNodeTypeWrapper(file, funcParam, { flags: SymbolTypeFlag.runtime }),
180
                            nameRange: funcParam.tokens.name.location?.range
3,102✔
181
                        });
182
                    }
183
                });
184
                // validate only what's needed in the file
185

186
                const segmentsToWalkForValidation = thisFileHasChanges
1,559✔
187
                    ? file.validationSegmenter.getAllUnvalidatedSegments()
1,559✔
188
                    : file.validationSegmenter.getSegmentsWithChangedSymbols(this.event.changedSymbols);
189

190
                let segmentsValidated = 0;
1,559✔
191
                for (const segment of segmentsToWalkForValidation) {
1,559✔
192
                    if (!file.validationSegmenter.checkIfSegmentNeedsRevalidation(segment, this.event.changedSymbols)) {
2,998!
193
                        continue;
×
194
                    }
195
                    this.currentSegmentBeingValidated = segment;
2,998✔
196
                    this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, segment: segment, tag: ScopeValidatorDiagnosticTag.Segment });
2,998✔
197
                    segmentsValidated++;
2,998✔
198
                    segment.walk(validationVisitor, {
2,998✔
199
                        walkMode: InsideSegmentWalkMode
200
                    });
201
                    file.markSegmentAsValidated(segment);
2,998✔
202
                    this.currentSegmentBeingValidated = null;
2,998✔
203
                }
204
                this.metrics.set(file.pkgPath, segmentsValidated);
1,559✔
205
            }
206
        });
207
    }
208

209
    private doesFileProvideChangedSymbol(file: BrsFile, changedSymbols: Map<SymbolTypeFlag, Set<string>>) {
210
        if (!changedSymbols) {
143!
211
            return true;
×
212
        }
213
        for (const flag of [SymbolTypeFlag.runtime, SymbolTypeFlag.typetime]) {
143✔
214
            const providedSymbolKeysFlag = file.providedSymbols.symbolMap.get(flag).keys();
286✔
215
            const changedSymbolSetForFlag = changedSymbols.get(flag);
286✔
216

217
            for (let providedKey of providedSymbolKeysFlag) {
286✔
218
                if (changedSymbolSetForFlag.has(providedKey)) {
270!
219
                    return true;
×
220
                }
221
            }
222
        }
223
        return false;
143✔
224
    }
225

226
    private currentSegmentBeingValidated: AstNode;
227

228

229
    private isTypeKnown(exprType: BscType) {
230
        let isKnownType = exprType?.isResolvable();
3,478✔
231
        return isKnownType;
3,478✔
232
    }
233

234
    /**
235
     * If this is the lhs of an assignment, we don't need to flag it as unresolved
236
     */
237
    private hasValidDeclaration(expression: Expression, exprType: BscType, definingNode?: AstNode) {
238
        if (!isVariableExpression(expression)) {
3,478✔
239
            return false;
1,000✔
240
        }
241
        let assignmentAncestor: AssignmentStatement;
242
        if (isAssignmentStatement(definingNode) && definingNode.tokens.equals.kind === TokenKind.Equal) {
2,478✔
243
            // this symbol was defined in a "normal" assignment (eg. not a compound assignment)
244
            assignmentAncestor = definingNode;
295✔
245
            return assignmentAncestor?.tokens.name?.text.toLowerCase() === expression?.tokens.name?.text.toLowerCase();
295!
246
        } else if (isFunctionParameterExpression(definingNode)) {
2,183✔
247
            // this symbol was defined in a function param
248
            return true;
436✔
249
        } else {
250
            assignmentAncestor = expression?.findAncestor(isAssignmentStatement);
1,747!
251
        }
252
        return assignmentAncestor?.tokens.name === expression?.tokens.name && isUnionType(exprType);
1,747!
253
    }
254

255
    /**
256
     * Validate every function call to `CreateObject`.
257
     * Ideally we would create better type checking/handling for this, but in the mean time, we know exactly
258
     * what these calls are supposed to look like, and this is a very common thing for brs devs to do, so just
259
     * do this manually for now.
260
     */
261
    protected validateCreateObjectCall(file: BrsFile, call: CallExpression) {
262

263
        //skip non CreateObject function calls
264
        const callName = util.getAllDottedGetPartsAsString(call.callee)?.toLowerCase();
854✔
265
        if (callName !== 'createobject' || !isLiteralExpression(call?.args[0])) {
854!
266
            return;
793✔
267
        }
268
        const firstParamToken = (call?.args[0] as LiteralExpression)?.tokens?.value;
61!
269
        const firstParamStringValue = firstParamToken?.text?.replace(/"/g, '');
61!
270
        if (!firstParamStringValue) {
61!
271
            return;
×
272
        }
273
        const firstParamStringValueLower = firstParamStringValue.toLowerCase();
61✔
274

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

319
            // Test for deprecation
320
            if (brightScriptComponent?.isDeprecated) {
29!
321
                this.addDiagnostic({
×
322
                    ...DiagnosticMessages.deprecatedBrightScriptComponent(firstParamStringValue, brightScriptComponent.deprecatedDescription),
323
                    location: call.location
324
                });
325
            }
326
        }
327

328
    }
329

330
    /**
331
     * Detect calls to functions with the incorrect number of parameters, or wrong types of arguments
332
     */
333
    private validateFunctionCall(file: BrsFile, expression: CallExpression) {
334
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: {} };
854✔
335
        let funcType = this.getNodeTypeWrapper(file, expression?.callee, getTypeOptions);
854!
336
        if (funcType?.isResolvable() && isClassType(funcType)) {
854✔
337
            // We're calling a class - get the constructor
338
            funcType = funcType.getMemberType('new', getTypeOptions);
123✔
339
        }
340
        if (funcType?.isResolvable() && isTypedFunctionType(funcType)) {
854✔
341
            //funcType.setName(expression.callee. .name);
342

343
            //get min/max parameter count for callable
344
            let minParams = 0;
603✔
345
            let maxParams = 0;
603✔
346
            for (let param of funcType.params) {
603✔
347
                maxParams++;
832✔
348
                //optional parameters must come last, so we can assume that minParams won't increase once we hit
349
                //the first isOptional
350
                if (param.isOptional !== true) {
832✔
351
                    minParams++;
452✔
352
                }
353
            }
354
            if (funcType.isVariadic) {
603✔
355
                // function accepts variable number of arguments
356
                maxParams = CallExpression.MaximumArguments;
3✔
357
            }
358
            let expCallArgCount = expression.args.length;
603✔
359
            if (expCallArgCount > maxParams || expCallArgCount < minParams) {
603✔
360
                let minMaxParamsText = minParams === maxParams ? maxParams : `${minParams}-${maxParams}`;
31✔
361
                this.addMultiScopeDiagnostic({
31✔
362
                    ...DiagnosticMessages.mismatchArgumentCount(minMaxParamsText, expCallArgCount),
363
                    location: expression.callee.location
364
                });
365
            }
366
            let paramIndex = 0;
603✔
367
            for (let arg of expression.args) {
603✔
368
                const data = {} as ExtraSymbolData;
522✔
369
                let argType = this.getNodeTypeWrapper(file, arg, { flags: SymbolTypeFlag.runtime, data: data });
522✔
370

371
                const paramType = funcType.params[paramIndex]?.type;
522✔
372
                if (!paramType) {
522✔
373
                    // unable to find a paramType -- maybe there are more args than params
374
                    break;
16✔
375
                }
376

377
                if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) {
506✔
378
                    argType = data.definingNode.getConstructorType();
2✔
379
                }
380

381
                const compatibilityData: TypeCompatibilityData = {};
506✔
382
                const isAllowedArgConversion = this.checkAllowedArgConversions(paramType, argType);
506✔
383
                if (!isAllowedArgConversion && !paramType?.isTypeCompatible(argType, compatibilityData)) {
506!
384
                    this.addMultiScopeDiagnostic({
32✔
385
                        ...DiagnosticMessages.argumentTypeMismatch(argType.toString(), paramType.toString(), compatibilityData),
386
                        location: arg.location
387
                    });
388
                }
389
                paramIndex++;
506✔
390
            }
391

392
        }
393
    }
394

395
    private checkAllowedArgConversions(paramType: BscType, argType: BscType): boolean {
396
        if (isNumberType(argType) && isBooleanType(paramType)) {
506✔
397
            return true;
8✔
398
        }
399
        return false;
498✔
400
    }
401

402

403
    /**
404
     * Detect return statements with incompatible types vs. declared return type
405
     */
406
    private validateReturnStatement(file: BrsFile, returnStmt: ReturnStatement) {
407
        const data: ExtraSymbolData = {};
260✔
408
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: data };
260✔
409
        let funcType = returnStmt.findAncestor(isFunctionExpression).getType({ flags: SymbolTypeFlag.typetime });
260✔
410
        if (isTypedFunctionType(funcType)) {
260!
411
            const actualReturnType = this.getNodeTypeWrapper(file, returnStmt?.value, getTypeOptions);
260!
412
            const compatibilityData: TypeCompatibilityData = {};
260✔
413

414
            if (funcType.returnType.isResolvable() && actualReturnType && !funcType.returnType.isTypeCompatible(actualReturnType, compatibilityData)) {
260✔
415
                this.addMultiScopeDiagnostic({
12✔
416
                    ...DiagnosticMessages.returnTypeMismatch(actualReturnType.toString(), funcType.returnType.toString(), compatibilityData),
417
                    location: returnStmt.value.location
418
                });
419

420
            }
421
        }
422
    }
423

424
    /**
425
     * Detect assigned type different from expected member type
426
     */
427
    private validateDottedSetStatement(file: BrsFile, dottedSetStmt: DottedSetStatement) {
428
        const typeChainExpectedLHS = [] as TypeChainEntry[];
82✔
429
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
82✔
430

431
        const expectedLHSType = this.getNodeTypeWrapper(file, dottedSetStmt, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
82✔
432
        const actualRHSType = this.getNodeTypeWrapper(file, dottedSetStmt?.value, getTypeOpts);
82!
433
        const compatibilityData: TypeCompatibilityData = {};
82✔
434
        const typeChainScan = util.processTypeChain(typeChainExpectedLHS);
82✔
435
        // check if anything in typeChain is an AA - if so, just allow it
436
        if (typeChainExpectedLHS.find(typeChainItem => isAssociativeArrayType(typeChainItem.type))) {
133✔
437
            // something in the chain is an AA
438
            // treat members as dynamic - they could have been set without the type system's knowledge
439
            return;
37✔
440
        }
441
        if (!expectedLHSType || !expectedLHSType?.isResolvable()) {
45!
442
            this.addMultiScopeDiagnostic({
7✔
443
                ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
444
                location: typeChainScan?.location
21!
445
            });
446
            return;
7✔
447
        }
448

449
        let accessibilityIsOk = this.checkMemberAccessibility(file, dottedSetStmt, typeChainExpectedLHS);
38✔
450

451
        //Most Component fields can be set with strings
452
        //TODO: be more precise about which fields can actually accept strings
453
        //TODO: if RHS is a string literal, we can do more validation to make sure it's the correct type
454
        if (isComponentType(dottedSetStmt.obj?.getType({ flags: SymbolTypeFlag.runtime }))) {
38!
455
            if (isStringType(actualRHSType)) {
12✔
456
                return;
4✔
457
            }
458
        }
459

460
        if (accessibilityIsOk && !expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
34!
461
            this.addMultiScopeDiagnostic({
7✔
462
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
463
                location: dottedSetStmt.location
464
            });
465
        }
466
    }
467

468
    /**
469
     * Detect when declared type does not match rhs type
470
     */
471
    private validateAssignmentStatement(file: BrsFile, assignStmt: AssignmentStatement) {
472
        if (!assignStmt?.typeExpression) {
595!
473
            // nothing to check
474
            return;
588✔
475
        }
476

477
        const typeChainExpectedLHS = [];
7✔
478
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
7✔
479
        const expectedLHSType = this.getNodeTypeWrapper(file, assignStmt.typeExpression, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
7✔
480
        const actualRHSType = this.getNodeTypeWrapper(file, assignStmt.value, getTypeOpts);
7✔
481
        const compatibilityData: TypeCompatibilityData = {};
7✔
482
        if (!expectedLHSType || !expectedLHSType.isResolvable()) {
7✔
483
            // LHS is not resolvable... handled elsewhere
484
        } else if (!expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
6!
485
            this.addMultiScopeDiagnostic({
1✔
486
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
487
                location: assignStmt.location
488
            });
489
        }
490
    }
491

492
    /**
493
     * Detect invalid use of a binary operator
494
     */
495
    private validateBinaryExpression(file: BrsFile, binaryExpr: BinaryExpression | AugmentedAssignmentStatement) {
496
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
277✔
497

498
        if (util.isInTypeExpression(binaryExpr)) {
277✔
499
            return;
13✔
500
        }
501

502
        let leftType = isBinaryExpression(binaryExpr)
264✔
503
            ? this.getNodeTypeWrapper(file, binaryExpr.left, getTypeOpts)
264✔
504
            : this.getNodeTypeWrapper(file, binaryExpr.item, getTypeOpts);
505
        let rightType = isBinaryExpression(binaryExpr)
264✔
506
            ? this.getNodeTypeWrapper(file, binaryExpr.right, getTypeOpts)
264✔
507
            : this.getNodeTypeWrapper(file, binaryExpr.value, getTypeOpts);
508

509
        if (!leftType.isResolvable() || !rightType.isResolvable()) {
264✔
510
            // Can not find the type. error handled elsewhere
511
            return;
11✔
512
        }
513
        let leftTypeToTest = leftType;
253✔
514
        let rightTypeToTest = rightType;
253✔
515

516
        if (isEnumMemberType(leftType) || isEnumType(leftType)) {
253✔
517
            leftTypeToTest = leftType.underlyingType;
11✔
518
        }
519
        if (isEnumMemberType(rightType) || isEnumType(rightType)) {
253✔
520
            rightTypeToTest = rightType.underlyingType;
10✔
521
        }
522

523
        if (isUnionType(leftType) || isUnionType(rightType)) {
253!
524
            // TODO: it is possible to validate based on innerTypes, but more complicated
525
            // Because you need to verify each combination of types
UNCOV
526
            return;
×
527
        }
528
        const leftIsPrimitive = isPrimitiveType(leftTypeToTest);
253✔
529
        const rightIsPrimitive = isPrimitiveType(rightTypeToTest);
253✔
530
        const leftIsAny = isDynamicType(leftTypeToTest) || isObjectType(leftTypeToTest);
253✔
531
        const rightIsAny = isDynamicType(rightTypeToTest) || isObjectType(rightTypeToTest);
253✔
532

533

534
        if (leftIsAny && rightIsAny) {
253✔
535
            // both operands are basically "any" type... ignore;
536
            return;
19✔
537
        } else if ((leftIsAny && rightIsPrimitive) || (leftIsPrimitive && rightIsAny)) {
234✔
538
            // one operand is basically "any" type... ignore;
539
            return;
43✔
540
        }
541
        const opResult = util.binaryOperatorResultType(leftTypeToTest, binaryExpr.tokens.operator, rightTypeToTest);
191✔
542

543
        if (isDynamicType(opResult)) {
191✔
544
            // if the result was dynamic, that means there wasn't a valid operation
545
            this.addMultiScopeDiagnostic({
7✔
546
                ...DiagnosticMessages.operatorTypeMismatch(binaryExpr.tokens.operator.text, leftType.toString(), rightType.toString()),
547
                location: binaryExpr.location
548
            });
549
        }
550
    }
551

552
    /**
553
     * Detect invalid use of a Unary operator
554
     */
555
    private validateUnaryExpression(file: BrsFile, unaryExpr: UnaryExpression) {
556
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
33✔
557

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

560
        if (!rightType.isResolvable()) {
33!
561
            // Can not find the type. error handled elsewhere
UNCOV
562
            return;
×
563
        }
564
        let rightTypeToTest = rightType;
33✔
565
        if (isEnumMemberType(rightType)) {
33!
UNCOV
566
            rightTypeToTest = rightType.underlyingType;
×
567
        }
568

569

570
        if (isUnionType(rightTypeToTest)) {
33✔
571
            // TODO: it is possible to validate based on innerTypes, but more complicated
572
            // Because you need to verify each combination of types
573

574
        } else if (isDynamicType(rightTypeToTest) || isObjectType(rightTypeToTest)) {
32✔
575
            // operand is basically "any" type... ignore;
576

577
        } else if (isPrimitiveType(rightType)) {
27!
578
            const opResult = util.unaryOperatorResultType(unaryExpr.tokens.operator, rightTypeToTest);
27✔
579
            if (isDynamicType(opResult)) {
27✔
580
                this.addMultiScopeDiagnostic({
1✔
581
                    ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
582
                    location: unaryExpr.location
583
                });
584
            }
585
        } else {
586
            // rhs is not a primitive, so no binary operator is allowed
UNCOV
587
            this.addMultiScopeDiagnostic({
×
588
                ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
589
                location: unaryExpr.location
590
            });
591
        }
592
    }
593

594
    private validateIncrementStatement(file: BrsFile, incStmt: IncrementStatement) {
595
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
9✔
596

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

599
        if (!rightType.isResolvable()) {
9!
600
            // Can not find the type. error handled elsewhere
UNCOV
601
            return;
×
602
        }
603

604
        if (isUnionType(rightType)) {
9!
605
            // TODO: it is possible to validate based on innerTypes, but more complicated
606
            // because you need to verify each combination of types
607
        } else if (isDynamicType(rightType) || isObjectType(rightType)) {
9✔
608
            // operand is basically "any" type... ignore
609
        } else if (isNumberType(rightType)) {
7✔
610
            // operand is a number.. this is ok
611
        } else {
612
            // rhs is not a number, so no increment operator is not allowed
613
            this.addMultiScopeDiagnostic({
1✔
614
                ...DiagnosticMessages.operatorTypeMismatch(incStmt.tokens.operator.text, rightType.toString()),
615
                location: incStmt.location
616
            });
617
        }
618
    }
619

620

621
    validateVariableAndDottedGetExpressions(file: BrsFile, expression: VariableExpression | DottedGetExpression) {
622
        if (isDottedGetExpression(expression.parent)) {
4,784✔
623
            // We validate dottedGetExpressions at the top-most level
624
            return;
1,303✔
625
        }
626
        if (isVariableExpression(expression)) {
3,481✔
627
            if (isAssignmentStatement(expression.parent) && expression.parent.tokens.name === expression.tokens.name) {
2,481!
628
                // Don't validate LHS of assignments
UNCOV
629
                return;
×
630
            } else if (isNamespaceStatement(expression.parent)) {
2,481✔
631
                return;
3✔
632
            }
633
        }
634

635
        let symbolType = SymbolTypeFlag.runtime;
3,478✔
636
        let oppositeSymbolType = SymbolTypeFlag.typetime;
3,478✔
637
        const isUsedAsType = util.isInTypeExpression(expression);
3,478✔
638
        if (isUsedAsType) {
3,478✔
639
            // This is used in a TypeExpression - only look up types from SymbolTable
640
            symbolType = SymbolTypeFlag.typetime;
1,102✔
641
            oppositeSymbolType = SymbolTypeFlag.runtime;
1,102✔
642
        }
643

644
        // Do a complete type check on all DottedGet and Variable expressions
645
        // this will create a diagnostic if an invalid member is accessed
646
        const typeChain: TypeChainEntry[] = [];
3,478✔
647
        const typeData = {} as ExtraSymbolData;
3,478✔
648
        let exprType = this.getNodeTypeWrapper(file, expression, {
3,478✔
649
            flags: symbolType,
650
            typeChain: typeChain,
651
            data: typeData
652
        });
653

654
        const hasValidDeclaration = this.hasValidDeclaration(expression, exprType, typeData?.definingNode);
3,478!
655

656
        //include a hint diagnostic if this type is marked as deprecated
657
        if (typeData.flags & SymbolTypeFlag.deprecated) { // eslint-disable-line no-bitwise
3,478✔
658
            this.addMultiScopeDiagnostic({
2✔
659
                ...DiagnosticMessages.itemIsDeprecated(),
660
                location: expression.tokens.name.location,
661
                tags: [DiagnosticTag.Deprecated]
662
            });
663
        }
664

665
        if (!this.isTypeKnown(exprType) && !hasValidDeclaration) {
3,478✔
666
            if (this.getNodeTypeWrapper(file, expression, { flags: oppositeSymbolType, isExistenceTest: true })?.isResolvable()) {
228!
667
                const oppoSiteTypeChain = [];
5✔
668
                const invalidlyUsedResolvedType = this.getNodeTypeWrapper(file, expression, { flags: oppositeSymbolType, typeChain: oppoSiteTypeChain, isExistenceTest: true });
5✔
669
                const typeChainScan = util.processTypeChain(oppoSiteTypeChain);
5✔
670
                if (isUsedAsType) {
5✔
671
                    this.addMultiScopeDiagnostic({
2✔
672
                        ...DiagnosticMessages.itemCannotBeUsedAsType(typeChainScan.fullChainName),
673
                        location: expression.location
674
                    });
675
                } else if (invalidlyUsedResolvedType && !isReferenceType(invalidlyUsedResolvedType)) {
3✔
676
                    if (!isAliasStatement(expression.parent)) {
1!
677
                        // alias rhs CAN be a type!
UNCOV
678
                        this.addMultiScopeDiagnostic({
×
679
                            ...DiagnosticMessages.itemCannotBeUsedAsVariable(invalidlyUsedResolvedType.toString()),
680
                            location: expression.location
681
                        });
682
                    }
683
                } else {
684
                    const typeChainScan = util.processTypeChain(typeChain);
2✔
685
                    //if this is a function call, provide a different diganostic code
686
                    if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
2✔
687
                        this.addMultiScopeDiagnostic({
1✔
688
                            ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
689
                            location: typeChainScan?.location
3!
690
                        });
691
                    } else {
692
                        this.addMultiScopeDiagnostic({
1✔
693
                            ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
694
                            location: typeChainScan?.location
3!
695
                        });
696
                    }
697
                }
698

699
            } else if (!typeData?.isFromDocComment) {
223!
700
                // only show "cannot find... " errors if the type is not defined from a doc comment
701
                const typeChainScan = util.processTypeChain(typeChain);
222✔
702
                if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
222✔
703
                    this.addMultiScopeDiagnostic({
25✔
704
                        ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
705
                        location: typeChainScan?.location
75!
706
                    });
707
                } else {
708
                    this.addMultiScopeDiagnostic({
197✔
709
                        ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
710
                        location: typeChainScan?.location
591!
711
                    });
712
                }
713

714
            }
715
        }
716
        if (isUsedAsType) {
3,478✔
717
            return;
1,102✔
718
        }
719

720
        const containingNamespace = expression.findAncestor<NamespaceStatement>(isNamespaceStatement);
2,376✔
721
        const containingNamespaceName = containingNamespace?.getName(ParseMode.BrighterScript);
2,376✔
722

723
        if (!(isCallExpression(expression.parent) && isNewExpression(expression.parent?.parent))) {
2,376!
724
            const classUsedAsVarEntry = this.checkTypeChainForClassUsedAsVar(typeChain, containingNamespaceName);
2,262✔
725
            const isClassInNamespace = containingNamespace?.getSymbolTable().hasSymbol(typeChain[0].name, SymbolTypeFlag.runtime);
2,262✔
726
            if (classUsedAsVarEntry && !isClassInNamespace) {
2,262!
727

UNCOV
728
                this.addMultiScopeDiagnostic({
×
729
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable(classUsedAsVarEntry.toString()),
730
                    location: expression.location
731
                });
UNCOV
732
                return;
×
733
            }
734
        }
735

736
        const lastTypeInfo = typeChain[typeChain.length - 1];
2,376✔
737
        const parentTypeInfo = typeChain[typeChain.length - 2];
2,376✔
738

739
        this.checkMemberAccessibility(file, expression, typeChain);
2,376✔
740

741
        if (isNamespaceType(exprType) && !isAliasStatement(expression.parent)) {
2,376✔
742
            this.addMultiScopeDiagnostic({
22✔
743
                ...DiagnosticMessages.itemCannotBeUsedAsVariable('namespace'),
744
                location: expression.location
745
            });
746
        } else if (isEnumType(exprType) && !isAliasStatement(expression.parent)) {
2,354✔
747
            const enumStatement = this.event.scope.getEnum(util.getAllDottedGetPartsAsString(expression));
15✔
748
            if (enumStatement) {
15✔
749
                // there's an enum with this name
750
                this.addMultiScopeDiagnostic({
2✔
751
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable('enum'),
752
                    location: expression.location
753
                });
754
            }
755
        } else if (isDynamicType(exprType) && isEnumType(parentTypeInfo?.type) && isDottedGetExpression(expression)) {
2,339✔
756
            const enumFileLink = this.event.scope.getEnumFileLink(util.getAllDottedGetPartsAsString(expression.obj));
8✔
757
            const typeChainScanForParent = util.processTypeChain(typeChain.slice(0, -1));
8✔
758
            if (enumFileLink) {
8✔
759
                this.addMultiScopeDiagnostic({
5✔
760
                    ...DiagnosticMessages.unknownEnumValue(lastTypeInfo?.name, typeChainScanForParent.fullChainName),
15!
761
                    location: lastTypeInfo?.location,
15!
762
                    relatedInformation: [{
763
                        message: 'Enum declared here',
764
                        location: util.createLocationFromRange(
765
                            util.pathToUri(enumFileLink?.file.srcPath),
15!
766
                            enumFileLink?.item?.tokens.name.location?.range
45!
767
                        )
768
                    }]
769
                });
770
            }
771
        }
772
    }
773

774
    private checkTypeChainForClassUsedAsVar(typeChain: TypeChainEntry[], containingNamespaceName: string) {
775
        const ignoreKinds = [AstNodeKind.TypecastExpression, AstNodeKind.NewExpression];
2,262✔
776
        let lowerNameSoFar = '';
2,262✔
777
        let classUsedAsVar;
778
        let isFirst = true;
2,262✔
779
        for (let i = 0; i < typeChain.length - 1; i++) { // do not look at final entry - we CAN use the constructor as a variable
2,262✔
780
            const tce = typeChain[i];
1,217✔
781
            lowerNameSoFar += `${lowerNameSoFar ? '.' : ''}${tce.name.toLowerCase()}`;
1,217✔
782
            if (!isNamespaceType(tce.type)) {
1,217✔
783
                if (isFirst && containingNamespaceName) {
561✔
784
                    lowerNameSoFar = `${containingNamespaceName.toLowerCase()}.${lowerNameSoFar}`;
71✔
785
                }
786
                if (!tce.astNode || ignoreKinds.includes(tce.astNode.kind)) {
561✔
787
                    break;
12✔
788
                } else if (isClassType(tce.type) && lowerNameSoFar.toLowerCase() === tce.type.name.toLowerCase()) {
549✔
789
                    classUsedAsVar = tce.type;
1✔
790
                }
791
                break;
549✔
792
            }
793
            isFirst = false;
656✔
794
        }
795

796
        return classUsedAsVar;
2,262✔
797
    }
798

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

829
                    // eslint-disable-next-line no-bitwise
830
                    if (childChainItem.data.flags & SymbolTypeFlag.protected) {
147✔
831
                        const containingClassName = containingClassStmt?.getName(ParseMode.BrighterScript);
13✔
832
                        const containingNamespaceName = expression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
13✔
833
                        const ancestorClasses = this.event.scope.getClassHierarchy(containingClassName, containingNamespaceName).map(link => link.item);
13✔
834
                        const isSubClassOfDefiningClass = ancestorClasses.includes(classStmtThatDefinesChildMember);
13✔
835

836
                        if (!isSubClassOfDefiningClass) {
13✔
837
                            this.addMultiScopeDiagnostic({
5✔
838
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
839
                                location: expression.location
840
                            });
841
                            // there's an error... don't worry about the rest of the chain
842
                            return false;
5✔
843
                        }
844
                    }
845
                }
846

847
            }
848
        }
849
        return true;
2,405✔
850
    }
851

852
    /**
853
     * Find all "new" statements in the program,
854
     * and make sure we can find a class with that name
855
     */
856
    private validateNewExpression(file: BrsFile, newExpression: NewExpression) {
857
        const newExprType = this.getNodeTypeWrapper(file, newExpression, { flags: SymbolTypeFlag.typetime });
114✔
858
        if (isClassType(newExprType)) {
114✔
859
            return;
106✔
860
        }
861

862
        let potentialClassName = newExpression.className.getName(ParseMode.BrighterScript);
8✔
863
        const namespaceName = newExpression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
8!
864
        let newableClass = this.event.scope.getClass(potentialClassName, namespaceName);
8✔
865

866
        if (!newableClass) {
8!
867
            //try and find functions with this name.
868
            let fullName = util.getFullyQualifiedClassName(potentialClassName, namespaceName);
8✔
869

870
            this.addMultiScopeDiagnostic({
8✔
871
                ...DiagnosticMessages.expressionIsNotConstructable(fullName),
872
                location: newExpression.className.location
873
            });
874

875
        }
876
    }
877

878
    /**
879
     * Create diagnostics for any duplicate function declarations
880
     */
881
    private flagDuplicateFunctionDeclarations() {
882
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration });
1,591✔
883

884
        //for each list of callables with the same name
885
        for (let [lowerName, callableContainers] of this.event.scope.getCallableContainerMap()) {
1,591✔
886

887
            let globalCallables = [] as CallableContainer[];
119,451✔
888
            let nonGlobalCallables = [] as CallableContainer[];
119,451✔
889
            let ownCallables = [] as CallableContainer[];
119,451✔
890
            let ancestorNonGlobalCallables = [] as CallableContainer[];
119,451✔
891

892

893
            for (let container of callableContainers) {
119,451✔
894
                if (container.scope === this.event.program.globalScope) {
125,849✔
895
                    globalCallables.push(container);
124,098✔
896
                } else {
897
                    nonGlobalCallables.push(container);
1,751✔
898
                    if (container.scope === this.event.scope) {
1,751✔
899
                        ownCallables.push(container);
1,721✔
900
                    } else {
901
                        ancestorNonGlobalCallables.push(container);
30✔
902
                    }
903
                }
904
            }
905

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

930
            //add error diagnostics about duplicate functions in the same scope
931
            if (ownCallables.length > 1) {
119,451✔
932

933
                for (let callableContainer of ownCallables) {
5✔
934
                    let callable = callableContainer.callable;
10✔
935
                    const related = [];
10✔
936
                    for (const ownCallable of ownCallables) {
10✔
937
                        const thatNameRange = ownCallable.callable.nameRange;
20✔
938
                        if (ownCallable.callable.nameRange !== callable.nameRange) {
20✔
939
                            related.push({
10✔
940
                                message: `Function declared here`,
941
                                location: util.createLocationFromRange(
942
                                    util.pathToUri(ownCallable.callable.file?.srcPath),
30!
943
                                    thatNameRange
944
                                )
945
                            });
946
                        }
947
                    }
948

949
                    this.addMultiScopeDiagnostic({
10✔
950
                        ...DiagnosticMessages.duplicateFunctionImplementation(callable.name),
951
                        location: util.createLocationFromFileRange(callable.file, callable.nameRange),
952
                        relatedInformation: related
953
                    }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
954
                }
955
            }
956
        }
957
    }
958

959
    /**
960
     * Verify that all of the scripts imported by each file in this scope actually exist, and have the correct case
961
     */
962
    private validateScriptImportPaths() {
963
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Imports });
1,591✔
964

965
        let scriptImports = this.event.scope.getOwnScriptImports();
1,591✔
966
        //verify every script import
967
        for (let scriptImport of scriptImports) {
1,591✔
968
            let referencedFile = this.event.scope.getFileByRelativePath(scriptImport.destPath);
496✔
969
            //if we can't find the file
970
            if (!referencedFile) {
496✔
971
                //skip the default bslib file, it will exist at transpile time but should not show up in the program during validation cycle
972
                if (scriptImport.destPath === this.event.program.bslibPkgPath) {
17✔
973
                    continue;
2✔
974
                }
975
                let dInfo: DiagnosticInfo;
976
                if (scriptImport.text.trim().length === 0) {
15✔
977
                    dInfo = DiagnosticMessages.scriptSrcCannotBeEmpty();
1✔
978
                } else {
979
                    dInfo = DiagnosticMessages.referencedFileDoesNotExist();
14✔
980
                }
981

982
                this.addMultiScopeDiagnostic({
15✔
983
                    ...dInfo,
984
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
985
                }, ScopeValidatorDiagnosticTag.Imports);
986
                //if the character casing of the script import path does not match that of the actual path
987
            } else if (scriptImport.destPath !== referencedFile.destPath) {
479✔
988
                this.addMultiScopeDiagnostic({
2✔
989
                    ...DiagnosticMessages.scriptImportCaseMismatch(referencedFile.destPath),
990
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
991
                }, ScopeValidatorDiagnosticTag.Imports);
992
            }
993
        }
994
    }
995

996
    /**
997
     * Validate all classes defined in this scope
998
     */
999
    private validateClasses() {
1000
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Classes });
1,591✔
1001

1002
        let validator = new BsClassValidator(this.event.scope);
1,591✔
1003
        validator.validate();
1,591✔
1004
        for (const diagnostic of validator.diagnostics) {
1,591✔
1005
            this.addMultiScopeDiagnostic({
29✔
1006
                ...diagnostic
1007
            }, ScopeValidatorDiagnosticTag.Classes);
1008
        }
1009
    }
1010

1011

1012
    /**
1013
     * Find various function collisions
1014
     */
1015
    private diagnosticDetectFunctionCollisions(file: BrsFile) {
1016
        const fileUri = util.pathToUri(file.srcPath);
1,827✔
1017
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, tag: ScopeValidatorDiagnosticTag.FunctionCollisions });
1,827✔
1018
        for (let func of file.callables) {
1,827✔
1019
            const funcName = func.getName(ParseMode.BrighterScript);
1,590✔
1020
            const lowerFuncName = funcName?.toLowerCase();
1,590!
1021
            if (lowerFuncName) {
1,590!
1022

1023
                //find function declarations with the same name as a stdlib function
1024
                if (globalCallableMap.has(lowerFuncName)) {
1,590✔
1025
                    this.addMultiScopeDiagnostic({
5✔
1026
                        ...DiagnosticMessages.scopeFunctionShadowedByBuiltInFunction(),
1027
                        location: util.createLocationFromRange(fileUri, func.nameRange)
1028

1029
                    });
1030
                }
1031
            }
1032
        }
1033
    }
1034

1035
    public detectShadowedLocalVar(file: BrsFile, varDeclaration: { expr: AstNode; name: string; type: BscType; nameRange: Range }) {
1036
        const varName = varDeclaration.name;
1,653✔
1037
        const lowerVarName = varName.toLowerCase();
1,653✔
1038
        const callableContainerMap = this.event.scope.getCallableContainerMap();
1,653✔
1039
        const containingNamespace = varDeclaration.expr?.findAncestor<NamespaceStatement>(isNamespaceStatement);
1,653!
1040
        const localVarIsInNamespace = util.isVariableMemberOfNamespace(varDeclaration.name, varDeclaration.expr, containingNamespace);
1,653✔
1041

1042
        const varIsFunction = () => {
1,653✔
1043
            return isCallableType(varDeclaration.type);
11✔
1044
        };
1045

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

1104
    private validateXmlInterface(scope: XmlScope) {
1105
        if (!scope.xmlFile.parser.ast?.componentElement?.interfaceElement) {
429!
1106
            return;
398✔
1107
        }
1108
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLInterface });
31!
1109

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

1160
    /**
1161
     * Detect when a child has imported a script that an ancestor also imported
1162
     */
1163
    private diagnosticDetectDuplicateAncestorScriptImports(scope: XmlScope) {
1164
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLImports });
429!
1165
        if (scope.xmlFile.parentComponent) {
429✔
1166
            //build a lookup of pkg paths -> FileReference so we can more easily look up collisions
1167
            let parentScriptImports = scope.xmlFile.getAncestorScriptTagImports();
34✔
1168
            let lookup = {} as Record<string, FileReference>;
34✔
1169
            for (let parentScriptImport of parentScriptImports) {
34✔
1170
                //keep the first occurance of a pkgPath. Parent imports are first in the array
1171
                if (!lookup[parentScriptImport.destPath]) {
30!
1172
                    lookup[parentScriptImport.destPath] = parentScriptImport;
30✔
1173
                }
1174
            }
1175

1176
            //add warning for every script tag that this file shares with an ancestor
1177
            for (let scriptImport of scope.xmlFile.scriptTagImports) {
34✔
1178
                let ancestorScriptImport = lookup[scriptImport.destPath];
30✔
1179
                if (ancestorScriptImport) {
30✔
1180
                    let ancestorComponent = ancestorScriptImport.sourceFile as XmlFile;
21✔
1181
                    let ancestorComponentName = ancestorComponent.componentName?.text ?? ancestorComponent.destPath;
21!
1182
                    this.addDiagnostic({
21✔
1183
                        location: util.createLocationFromFileRange(scope.xmlFile, scriptImport.filePathRange),
1184
                        ...DiagnosticMessages.unnecessaryScriptImportInChildFromParent(ancestorComponentName)
1185
                    }, ScopeValidatorDiagnosticTag.XMLImports);
1186
                }
1187
            }
1188
        }
1189
    }
1190

1191
    /**
1192
     * Wraps the AstNode.getType() method, so that we can do extra processing on the result based on the current file
1193
     * In particular, since BrightScript does not support Unions, and there's no way to cast them to something else
1194
     * if the result of .getType() is a union, and we're in a .brs (brightScript) file, treat the result as Dynamic
1195
     *
1196
     * In most cases, this returns the result of node.getType()
1197
     *
1198
     * @param file the current file being processed
1199
     * @param node the node to get the type of
1200
     * @param getTypeOpts any options to pass to node.getType()
1201
     * @returns the processed result type
1202
     */
1203
    private getNodeTypeWrapper(file: BrsFile, node: AstNode, getTypeOpts: GetTypeOptions) {
1204
        const type = node?.getType(getTypeOpts);
7,862✔
1205

1206
        if (file.parseMode === ParseMode.BrightScript) {
7,862✔
1207
            // this is a brightscript file
1208
            const typeChain = getTypeOpts.typeChain;
933✔
1209
            if (typeChain) {
933✔
1210
                const hasUnion = typeChain.reduce((hasUnion, tce) => {
324✔
1211
                    return hasUnion || isUnionType(tce.type);
372✔
1212
                }, false);
1213
                if (hasUnion) {
324✔
1214
                    // there was a union somewhere in the typechain
1215
                    return DynamicType.instance;
6✔
1216
                }
1217
            }
1218
            if (isUnionType(type)) {
927✔
1219
                //this is a union
1220
                return DynamicType.instance;
4✔
1221
            }
1222
        }
1223

1224
        // by default return the result of node.getType()
1225
        return type;
7,852✔
1226
    }
1227

1228
    private getParentTypeDescriptor(typeChainResult: TypeChainProcessResult) {
1229
        if (typeChainResult.itemParentTypeKind === BscTypeKind.NamespaceType) {
231✔
1230
            return 'namespace';
117✔
1231
        }
1232
        return 'type';
114✔
1233
    }
1234

1235
    private addDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1236
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
51!
1237
        this.event.program.diagnostics.register(diagnostic, {
51✔
1238
            tags: [diagnosticTag],
1239
            segment: this.currentSegmentBeingValidated
1240
        });
1241
    }
1242

1243
    /**
1244
     * Add a diagnostic (to the first scope) that will have `relatedInformation` for each affected scope
1245
     */
1246
    private addMultiScopeDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1247
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
444✔
1248
        this.event.program.diagnostics.register(diagnostic, {
444✔
1249
            tags: [diagnosticTag],
1250
            segment: this.currentSegmentBeingValidated,
1251
            scope: this.event.scope
1252
        });
1253
    }
1254
}
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