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

rokucommunity / brighterscript / #14374

08 May 2025 08:02PM UTC coverage: 87.099% (-1.9%) from 89.017%
#14374

push

web-flow
Merge bb74432dc into 489231ac7

13489 of 16372 branches covered (82.39%)

Branch coverage included in aggregate %.

8031 of 8709 new or added lines in 103 files covered. (92.21%)

85 existing lines in 22 files now uncovered.

14462 of 15719 relevant lines covered (92.0%)

19990.89 hits per line

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

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

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

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

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

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

71
    private segmentsMetrics = new Map<string, { segments: number; time: string }>();
1,983✔
72
    private validationKindsMetrics = new Map<string, { timeMs: number; count: number }>();
1,983✔
73

74
    public processEvent(event: OnScopeValidateEvent) {
75
        this.event = event;
3,876✔
76
        if (this.event.program.globalScope === this.event.scope) {
3,876✔
77
            return;
1,982✔
78
        }
79
        const logger = this.event.program.logger;
1,894✔
80
        const metrics = {
1,894✔
81
            fileWalkTime: '',
82
            flagDuplicateFunctionTime: '',
83
            classValidationTime: '',
84
            scriptImportValidationTime: '',
85
            xmlValidationTime: ''
86
        };
87
        this.segmentsMetrics.clear();
1,894✔
88
        this.validationKindsMetrics.clear();
1,894✔
89
        const validationStopwatch = new Stopwatch();
1,894✔
90

91
        logger.time(LogLevel.debug, ['Validating scope', this.event.scope.name], () => {
1,894✔
92
            metrics.fileWalkTime = validationStopwatch.getDurationTextFor(() => {
1,894✔
93
                this.walkFiles();
1,894✔
94
            }).durationText;
95
            this.currentSegmentBeingValidated = null;
1,891✔
96
            metrics.flagDuplicateFunctionTime = validationStopwatch.getDurationTextFor(() => {
1,891✔
97
                this.flagDuplicateFunctionDeclarations();
1,891✔
98
            }).durationText;
99
            metrics.scriptImportValidationTime = validationStopwatch.getDurationTextFor(() => {
1,891✔
100
                this.validateScriptImportPaths();
1,891✔
101
            }).durationText;
102
            metrics.classValidationTime = validationStopwatch.getDurationTextFor(() => {
1,891✔
103
                this.validateClasses();
1,891✔
104
            }).durationText;
105
            metrics.xmlValidationTime = validationStopwatch.getDurationTextFor(() => {
1,891✔
106
                if (isXmlScope(this.event.scope)) {
1,891✔
107
                    //detect when the child imports a script that its ancestor also imports
108
                    this.diagnosticDetectDuplicateAncestorScriptImports(this.event.scope);
522✔
109
                    //validate component interface
110
                    this.validateXmlInterface(this.event.scope);
522✔
111
                }
112
            }).durationText;
113
        });
114
        logger.debug(this.event.scope.name, 'segment metrics:');
1,891✔
115
        let totalSegments = 0;
1,891✔
116
        for (const [filePath, metric] of this.segmentsMetrics) {
1,891✔
117
            this.event.program.logger.debug(' - ', filePath, metric.segments, metric.time);
1,815✔
118
            totalSegments += metric.segments;
1,815✔
119
        }
120
        logger.debug(this.event.scope.name, 'total segments validated', totalSegments);
1,891✔
121
        this.logValidationMetrics(metrics);
1,891✔
122
    }
123

124
    // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
125
    private logValidationMetrics(metrics: { [key: string]: number | string }) {
126
        let logs = [] as string[];
1,891✔
127
        for (let key in metrics) {
1,891✔
128
            logs.push(`${key}=${chalk.yellow(metrics[key].toString())}`);
9,455✔
129
        }
130
        this.event.program.logger.debug(`Validation Metrics (Scope: ${this.event.scope.name}): ${logs.join(', ')}`);
1,891✔
131
        let kindsLogs = [] as string[];
1,891✔
132
        const kindsArray = Array.from(this.validationKindsMetrics.keys()).sort();
1,891✔
133
        for (let key of kindsArray) {
1,891✔
134
            const timeData = this.validationKindsMetrics.get(key);
6,032✔
135
            kindsLogs.push(`${key}=${chalk.yellow(timeData.timeMs.toFixed(3).toString()) + 'ms'} (${timeData.count})`);
6,032✔
136
        }
137
        this.event.program.logger.debug(`Validation Walk Metrics (Scope: ${this.event.scope.name}): ${kindsLogs.join(', ')}`);
1,891✔
138
    }
139

140
    public reset() {
141
        this.event = undefined;
1,543✔
142
    }
143

144
    private walkFiles() {
145
        const hasChangeInfo = this.event.changedFiles && this.event.changedSymbols;
1,894✔
146

147
        //do many per-file checks for every file in this (and parent) scopes
148
        this.event.scope.enumerateBrsFiles((file) => {
1,894✔
149
            if (!isBrsFile(file)) {
2,287!
NEW
150
                return;
×
151
            }
152

153
            const thisFileHasChanges = this.event.changedFiles.includes(file);
2,287✔
154

155
            if (thisFileHasChanges || this.doesFileProvideChangedSymbol(file, this.event.changedSymbols)) {
2,287✔
156
                this.diagnosticDetectFunctionCollisions(file);
2,226✔
157
            }
158
        });
159
        const fileWalkStopWatch = new Stopwatch();
1,894✔
160

161
        this.event.scope.enumerateOwnFiles((file) => {
1,894✔
162
            if (isBrsFile(file)) {
2,799✔
163

164
                if (this.event.program.diagnostics.shouldFilterFile(file)) {
2,277!
NEW
165
                    return;
×
166
                }
167

168
                fileWalkStopWatch.reset();
2,277✔
169
                fileWalkStopWatch.start();
2,277✔
170

171
                const fileUri = util.pathToUri(file.srcPath);
2,277✔
172
                const thisFileHasChanges = this.event.changedFiles.includes(file);
2,277✔
173

174
                const hasUnvalidatedSegments = file.validationSegmenter.hasUnvalidatedSegments();
2,277✔
175

176
                if (hasChangeInfo && !hasUnvalidatedSegments) {
2,277✔
177
                    return;
459✔
178
                }
179

180
                const validationVisitor = createVisitor({
1,818✔
181
                    VariableExpression: (varExpr) => {
182
                        this.addValidationKindMetric('VariableExpression', () => {
4,034✔
183
                            this.validateVariableAndDottedGetExpressions(file, varExpr);
4,034✔
184
                        });
185
                    },
186
                    DottedGetExpression: (dottedGet) => {
187
                        this.addValidationKindMetric('DottedGetExpression', () => {
1,572✔
188
                            this.validateVariableAndDottedGetExpressions(file, dottedGet);
1,572✔
189
                        });
190
                    },
191
                    CallExpression: (functionCall) => {
192
                        this.addValidationKindMetric('CallExpression', () => {
994✔
193
                            this.validateCallExpression(file, functionCall);
994✔
194
                            this.validateCreateObjectCall(file, functionCall);
994✔
195
                            this.validateComponentMethods(file, functionCall);
994✔
196
                        });
197
                    },
198
                    CallfuncExpression: (functionCall) => {
199
                        this.addValidationKindMetric('CallfuncExpression', () => {
48✔
200
                            this.validateCallFuncExpression(file, functionCall);
48✔
201
                        });
202
                    },
203
                    ReturnStatement: (returnStatement) => {
204
                        this.addValidationKindMetric('ReturnStatement', () => {
390✔
205
                            this.validateReturnStatement(file, returnStatement);
390✔
206
                        });
207
                    },
208
                    DottedSetStatement: (dottedSetStmt) => {
209
                        this.addValidationKindMetric('DottedSetStatement', () => {
101✔
210
                            this.validateDottedSetStatement(file, dottedSetStmt);
101✔
211
                        });
212
                    },
213
                    BinaryExpression: (binaryExpr) => {
214
                        this.addValidationKindMetric('BinaryExpression', () => {
280✔
215
                            this.validateBinaryExpression(file, binaryExpr);
280✔
216
                        });
217
                    },
218
                    UnaryExpression: (unaryExpr) => {
219
                        this.addValidationKindMetric('UnaryExpression', () => {
33✔
220
                            this.validateUnaryExpression(file, unaryExpr);
33✔
221
                        });
222
                    },
223
                    AssignmentStatement: (assignStmt) => {
224
                        this.addValidationKindMetric('AssignmentStatement', () => {
705✔
225
                            this.validateAssignmentStatement(file, assignStmt);
705✔
226
                            // Note: this also includes For statements
227
                            this.detectShadowedLocalVar(file, {
705✔
228
                                expr: assignStmt,
229
                                name: assignStmt.tokens.name.text,
230
                                type: this.getNodeTypeWrapper(file, assignStmt, { flags: SymbolTypeFlag.runtime }),
231
                                nameRange: assignStmt.tokens.name.location?.range
2,115✔
232
                            });
233
                        });
234
                    },
235
                    AugmentedAssignmentStatement: (binaryExpr) => {
236
                        this.addValidationKindMetric('AugmentedAssignmentStatement', () => {
62✔
237
                            this.validateBinaryExpression(file, binaryExpr);
62✔
238
                        });
239
                    },
240
                    IncrementStatement: (stmt) => {
241
                        this.addValidationKindMetric('IncrementStatement', () => {
11✔
242
                            this.validateIncrementStatement(file, stmt);
11✔
243
                        });
244
                    },
245
                    NewExpression: (newExpr) => {
246
                        this.addValidationKindMetric('NewExpression', () => {
121✔
247
                            this.validateNewExpression(file, newExpr);
121✔
248
                        });
249
                    },
250
                    ForEachStatement: (forEachStmt) => {
251
                        this.addValidationKindMetric('ForEachStatement', () => {
26✔
252
                            this.detectShadowedLocalVar(file, {
26✔
253
                                expr: forEachStmt,
254
                                name: forEachStmt.tokens.item.text,
255
                                type: this.getNodeTypeWrapper(file, forEachStmt, { flags: SymbolTypeFlag.runtime }),
256
                                nameRange: forEachStmt.tokens.item.location?.range
78✔
257
                            });
258
                        });
259
                    },
260
                    FunctionParameterExpression: (funcParam) => {
261
                        this.addValidationKindMetric('FunctionParameterExpression', () => {
1,154✔
262
                            this.detectShadowedLocalVar(file, {
1,154✔
263
                                expr: funcParam,
264
                                name: funcParam.tokens.name.text,
265
                                type: this.getNodeTypeWrapper(file, funcParam, { flags: SymbolTypeFlag.runtime }),
266
                                nameRange: funcParam.tokens.name.location?.range
3,462✔
267
                            });
268
                        });
269
                    },
270
                    FunctionExpression: (func) => {
271
                        if (file.isTypedef) {
2,180✔
272
                            return;
10✔
273
                        }
274
                        this.addValidationKindMetric('FunctionExpression', () => {
2,170✔
275
                            this.validateFunctionExpressionForReturn(func);
2,170✔
276
                        });
277
                    },
278
                    AstNode: (node) => {
279
                        //check for doc comments
280
                        if (!node.leadingTrivia || node.leadingTrivia.filter(triviaToken => triviaToken.kind === TokenKind.Comment).length === 0) {
26,568✔
281
                            return;
20,335✔
282
                        }
283
                        this.addValidationKindMetric('AstNode', () => {
251✔
284
                            this.validateDocComments(node);
251✔
285
                        });
286
                    }
287
                });
288
                // validate only what's needed in the file
289

290
                const segmentsToWalkForValidation = thisFileHasChanges
1,818✔
291
                    ? file.validationSegmenter.getAllUnvalidatedSegments()
1,818!
292
                    : file.validationSegmenter.getSegmentsWithChangedSymbols(this.event.changedSymbols);
293

294
                let segmentsValidated = 0;
1,818✔
295

296
                if (thisFileHasChanges) {
1,818!
297
                    // clear all ScopeValidatorSegment diagnostics for this file
298
                    this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, tag: ScopeValidatorDiagnosticTag.Segment });
1,818✔
299
                }
300

301

302
                for (const segment of segmentsToWalkForValidation) {
1,818✔
303
                    if (!thisFileHasChanges && !file.validationSegmenter.checkIfSegmentNeedsRevalidation(segment, this.event.changedSymbols)) {
3,538!
NEW
304
                        continue;
×
305
                    }
306
                    this.currentSegmentBeingValidated = segment;
3,538✔
307
                    if (!thisFileHasChanges) {
3,538!
308
                        // just clear the affected diagnostics
NEW
309
                        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, segment: segment, tag: ScopeValidatorDiagnosticTag.Segment });
×
310
                    }
311
                    segmentsValidated++;
3,538✔
312
                    segment.walk(validationVisitor, {
3,538✔
313
                        walkMode: InsideSegmentWalkMode
314
                    });
315
                    file.markSegmentAsValidated(segment);
3,535✔
316
                    this.currentSegmentBeingValidated = null;
3,535✔
317
                }
318
                fileWalkStopWatch.stop();
1,815✔
319
                const timeString = fileWalkStopWatch.getDurationText();
1,815✔
320
                this.segmentsMetrics.set(file.pkgPath, { segments: segmentsValidated, time: timeString });
1,815✔
321
            }
322
        });
323
    }
324

325
    private addValidationKindMetric(name: string, funcToTime: () => void) {
326
        if (!this.validationKindsMetrics.has(name)) {
11,952✔
327
            this.validationKindsMetrics.set(name, { timeMs: 0, count: 0 });
6,044✔
328
        }
329
        const timeData = this.validationKindsMetrics.get(name);
11,952✔
330
        const validationKindStopWatch = new Stopwatch();
11,952✔
331
        validationKindStopWatch.start();
11,952✔
332
        funcToTime();
11,952✔
333
        validationKindStopWatch.stop();
11,949✔
334
        this.validationKindsMetrics.set(name, { timeMs: timeData.timeMs + validationKindStopWatch.totalMilliseconds, count: timeData.count + 1 });
11,949✔
335
    }
336

337
    private doesFileProvideChangedSymbol(file: BrsFile, changedSymbols: Map<SymbolTypeFlag, Set<string>>) {
338
        if (!changedSymbols) {
62!
NEW
339
            return true;
×
340
        }
341
        for (const flag of [SymbolTypeFlag.runtime, SymbolTypeFlag.typetime]) {
62✔
342
            const providedSymbolKeysFlag = file.providedSymbols.symbolMap.get(flag).keys();
124✔
343
            const changedSymbolSetForFlag = changedSymbols.get(flag);
124✔
344

345
            for (let providedKey of providedSymbolKeysFlag) {
124✔
346
                if (changedSymbolSetForFlag.has(providedKey)) {
50✔
347
                    return true;
1✔
348
                }
349
            }
350
        }
351
        return false;
61✔
352
    }
353

354
    private currentSegmentBeingValidated: AstNode;
355

356

357
    private isTypeKnown(exprType: BscType) {
358
        let isKnownType = exprType?.isResolvable();
4,140✔
359
        return isKnownType;
4,140✔
360
    }
361

362
    /**
363
     * If this is the lhs of an assignment, we don't need to flag it as unresolved
364
     */
365
    private hasValidDeclaration(expression: Expression, exprType: BscType, definingNode?: AstNode) {
366
        if (!isVariableExpression(expression)) {
4,140✔
367
            return false;
1,140✔
368
        }
369
        let assignmentAncestor: AssignmentStatement;
370
        if (isAssignmentStatement(definingNode) && definingNode.tokens.equals.kind === TokenKind.Equal) {
3,000✔
371
            // this symbol was defined in a "normal" assignment (eg. not a compound assignment)
372
            assignmentAncestor = definingNode;
350✔
373
            return assignmentAncestor?.tokens.name?.text.toLowerCase() === expression?.tokens.name?.text.toLowerCase();
350!
374
        } else if (isFunctionParameterExpression(definingNode)) {
2,650✔
375
            // this symbol was defined in a function param
376
            return true;
567✔
377
        } else {
378
            assignmentAncestor = expression?.findAncestor(isAssignmentStatement);
2,083!
379
        }
380
        return assignmentAncestor?.tokens.name === expression?.tokens.name && isUnionType(exprType);
2,083!
381
    }
382

383
    /**
384
     * Validate every function call to `CreateObject`.
385
     * Ideally we would create better type checking/handling for this, but in the mean time, we know exactly
386
     * what these calls are supposed to look like, and this is a very common thing for brs devs to do, so just
387
     * do this manually for now.
388
     */
389
    protected validateCreateObjectCall(file: BrsFile, call: CallExpression) {
390

391
        //skip non CreateObject function calls
392
        const callName = util.getAllDottedGetPartsAsString(call.callee)?.toLowerCase();
994✔
393
        if (callName !== 'createobject' || !isLiteralExpression(call?.args[0])) {
994!
394
            return;
919✔
395
        }
396
        const firstParamToken = (call?.args[0] as LiteralExpression)?.tokens?.value;
75!
397
        const firstParamStringValue = firstParamToken?.text?.replace(/"/g, '');
75!
398
        if (!firstParamStringValue) {
75!
NEW
399
            return;
×
400
        }
401
        const firstParamStringValueLower = firstParamStringValue.toLowerCase();
75✔
402

403
        //if this is a `createObject('roSGNode'` call, only support known sg node types
404
        if (firstParamStringValueLower === 'rosgnode' && isLiteralExpression(call?.args[1])) {
75!
405
            const componentName: Token = call?.args[1]?.tokens.value;
31!
406
            this.checkComponentName(componentName);
31✔
407
            if (call?.args.length !== 2) {
31!
408
                // roSgNode should only ever have 2 args in `createObject`
409
                this.addDiagnostic({
1✔
410
                    ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, [2], call?.args.length),
3!
411
                    location: call.location
412
                });
413
            }
414
        } else if (!platformComponentNames.has(firstParamStringValueLower)) {
44✔
415
            this.addDiagnostic({
7✔
416
                ...DiagnosticMessages.unknownBrightScriptComponent(firstParamStringValue),
417
                location: firstParamToken.location
418
            });
419
        } else {
420
            // This is valid brightscript component
421
            // Test for invalid arg counts
422
            const brightScriptComponent: BRSComponentData = components[firstParamStringValueLower];
37✔
423
            // Valid arg counts for createObject are 1+ number of args for constructor
424
            let validArgCounts = brightScriptComponent?.constructors.map(cnstr => cnstr.params.length + 1);
38!
425
            if (validArgCounts.length === 0) {
37✔
426
                // no constructors for this component, so createObject only takes 1 arg
427
                validArgCounts = [1];
4✔
428
            }
429
            if (!validArgCounts.includes(call?.args.length)) {
37!
430
                // Incorrect number of arguments included in `createObject()`
431
                this.addDiagnostic({
4✔
432
                    ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, validArgCounts, call?.args.length),
12!
433
                    location: call.location
434
                });
435
            }
436

437
            // Test for deprecation
438
            if (brightScriptComponent?.isDeprecated) {
37!
NEW
439
                this.addDiagnostic({
×
440
                    ...DiagnosticMessages.itemIsDeprecated(firstParamStringValue, brightScriptComponent.deprecatedDescription),
441
                    location: call.location
442
                });
443
            }
444
        }
445

446
    }
447

448
    private checkComponentName(componentName: Token) {
449
        //don't validate any components with a colon in their name (probably component libraries, but regular components can have them too).
450
        if (!componentName || componentName?.text?.includes(':')) {
32!
451
            return;
4✔
452
        }
453
        //add diagnostic for unknown components
454
        const unquotedComponentName = componentName?.text?.replace(/"/g, '');
28!
455
        if (unquotedComponentName && !platformNodeNames.has(unquotedComponentName.toLowerCase()) && !this.event.program.getComponent(unquotedComponentName)) {
28✔
456
            this.addDiagnostic({
4✔
457
                ...DiagnosticMessages.unknownRoSGNode(unquotedComponentName),
458
                location: componentName.location
459
            });
460
        }
461
    }
462

463
    /**
464
     * Validate every method call to `component.callfunc()`, `component.createChild()`, etc.
465
     */
466
    protected validateComponentMethods(file: BrsFile, call: CallExpression) {
467
        const lowerMethodNamesChecked = ['callfunc', 'createchild'];
994✔
468
        if (!isDottedGetExpression(call.callee)) {
994✔
469
            return;
594✔
470
        }
471

472
        const callName = call.callee.tokens?.name?.text?.toLowerCase();
400!
473
        if (!callName || !lowerMethodNamesChecked.includes(callName) || !isLiteralExpression(call?.args[0])) {
400!
474
            return;
387✔
475
        }
476

477
        const callerType = call.callee.obj?.getType({ flags: SymbolTypeFlag.runtime });
13!
478
        if (!isComponentType(callerType)) {
13✔
479
            return;
2✔
480
        }
481
        const firstArgToken = call?.args[0]?.tokens.value;
11!
482
        if (callName === 'createchild') {
11✔
483
            this.checkComponentName(firstArgToken);
1✔
484
        } else if (callName === 'callfunc' && !util.isGenericNodeType(callerType)) {
10✔
485
            const funcType = util.getCallFuncType(call, firstArgToken, { flags: SymbolTypeFlag.runtime, ignoreCall: true });
7✔
486
            if (!funcType?.isResolvable()) {
7✔
487
                const functionName = firstArgToken.text.replace(/"/g, '');
3✔
488
                const functionFullname = `${callerType.toString()}@.${functionName}`;
3✔
489
                this.addMultiScopeDiagnostic({
3✔
490
                    ...DiagnosticMessages.cannotFindCallFuncFunction(functionName, functionFullname, callerType.toString()),
491
                    location: firstArgToken?.location
9!
492
                });
493
            } else {
494
                this.validateFunctionCall(file, funcType, firstArgToken.location, call.args, 1);
4✔
495
            }
496
        }
497
    }
498

499

500
    private validateCallExpression(file: BrsFile, expression: CallExpression) {
501
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: {} };
994✔
502
        let funcType = this.getNodeTypeWrapper(file, expression?.callee, getTypeOptions);
994!
503
        if (funcType?.isResolvable() && isClassType(funcType)) {
994✔
504
            // We're calling a class - get the constructor
505
            funcType = funcType.getMemberType('new', getTypeOptions);
131✔
506
        }
507
        const callErrorLocation = expression?.callee?.location;
994!
508
        return this.validateFunctionCall(file, funcType, callErrorLocation, expression.args);
994✔
509

510
    }
511

512
    private validateCallFuncExpression(file: BrsFile, expression: CallfuncExpression) {
513
        const callerType = expression.callee?.getType({ flags: SymbolTypeFlag.runtime });
48!
514
        if (isDynamicType(callerType)) {
48✔
515
            return;
20✔
516
        }
517
        const methodToken = expression.tokens.methodName;
28✔
518
        const methodName = methodToken?.text ?? '';
28✔
519
        const functionFullname = `${callerType.toString()}@.${methodName}`;
28✔
520
        const callErrorLocation = expression.location;
28✔
521
        if (util.isGenericNodeType(callerType) || isObjectType(callerType) || isDynamicType(callerType)) {
28✔
522
            // ignore "general" node
523
            return;
6✔
524
        }
525

526
        if (!isComponentType(callerType)) {
22✔
527
            this.addMultiScopeDiagnostic({
1✔
528
                ...DiagnosticMessages.cannotFindCallFuncFunction(methodName, functionFullname, callerType.toString()),
529
                location: callErrorLocation
530
            });
531
            return;
1✔
532
        }
533

534
        const funcType = util.getCallFuncType(expression, methodToken, { flags: SymbolTypeFlag.runtime, ignoreCall: true });
21✔
535
        if (!funcType?.isResolvable()) {
18✔
536
            this.addMultiScopeDiagnostic({
1✔
537
                ...DiagnosticMessages.cannotFindCallFuncFunction(methodName, functionFullname, callerType.toString()),
538
                location: callErrorLocation
539
            });
540
        }
541
        return this.validateFunctionCall(file, funcType, callErrorLocation, expression.args);
18✔
542
    }
543

544
    /**
545
     * Detect calls to functions with the incorrect number of parameters, or wrong types of arguments
546
     */
547
    private validateFunctionCall(file: BrsFile, funcType: BscType, callErrorLocation: Location, args: Expression[], argOffset = 0) {
1,012✔
548
        if (!funcType?.isResolvable() || !isTypedFunctionType(funcType)) {
1,016✔
549
            return;
276✔
550
        }
551

552
        //get min/max parameter count for callable
553
        let minParams = 0;
740✔
554
        let maxParams = 0;
740✔
555
        for (let param of funcType.params) {
740✔
556
            maxParams++;
1,020✔
557
            //optional parameters must come last, so we can assume that minParams won't increase once we hit
558
            //the first isOptional
559
            if (param.isOptional !== true) {
1,020✔
560
                minParams++;
550✔
561
            }
562
        }
563
        if (funcType.isVariadic) {
740✔
564
            // function accepts variable number of arguments
565
            maxParams = CallExpression.MaximumArguments;
12✔
566
        }
567
        const argsForCall = argOffset < 1 ? args : args.slice(argOffset);
740✔
568

569
        let expCallArgCount = argsForCall.length;
740✔
570
        if (expCallArgCount > maxParams || expCallArgCount < minParams) {
740✔
571
            let minMaxParamsText = minParams === maxParams ? maxParams + argOffset : `${minParams + argOffset}-${maxParams + argOffset}`;
33✔
572
            this.addMultiScopeDiagnostic({
33✔
573
                ...DiagnosticMessages.mismatchArgumentCount(minMaxParamsText, expCallArgCount + argOffset),
574
                location: callErrorLocation
575
            });
576
        }
577
        let paramIndex = 0;
740✔
578
        for (let arg of argsForCall) {
740✔
579
            const data = {} as ExtraSymbolData;
639✔
580
            let argType = this.getNodeTypeWrapper(file, arg, { flags: SymbolTypeFlag.runtime, data: data });
639✔
581

582
            const paramType = funcType.params[paramIndex]?.type;
639✔
583
            if (!paramType) {
639✔
584
                // unable to find a paramType -- maybe there are more args than params
585
                break;
22✔
586
            }
587

588
            if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) {
617✔
589
                argType = data.definingNode.getConstructorType();
2✔
590
            }
591

592
            const compatibilityData: TypeCompatibilityData = {};
617✔
593
            const isAllowedArgConversion = this.checkAllowedArgConversions(paramType, argType);
617✔
594
            if (!isAllowedArgConversion && !paramType?.isTypeCompatible(argType, compatibilityData)) {
617!
595
                this.addMultiScopeDiagnostic({
42✔
596
                    ...DiagnosticMessages.argumentTypeMismatch(argType?.toString() ?? 'unknown', paramType?.toString() ?? 'unknown', compatibilityData),
504!
597
                    location: arg.location
598
                });
599
            }
600
            paramIndex++;
617✔
601
        }
602
    }
603

604
    private checkAllowedArgConversions(paramType: BscType, argType: BscType): boolean {
605
        if (isNumberType(argType) && isBooleanType(paramType)) {
617✔
606
            return true;
8✔
607
        }
608
        return false;
609✔
609
    }
610

611

612
    /**
613
     * Detect return statements with incompatible types vs. declared return type
614
     */
615
    private validateReturnStatement(file: BrsFile, returnStmt: ReturnStatement) {
616
        const data: ExtraSymbolData = {};
391✔
617
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: data };
391✔
618
        let funcType = returnStmt.findAncestor(isFunctionExpression)?.getType({ flags: SymbolTypeFlag.typetime });
391✔
619
        if (isTypedFunctionType(funcType)) {
391✔
620
            let actualReturnType = returnStmt?.value
390!
621
                ? this.getNodeTypeWrapper(file, returnStmt?.value, getTypeOptions)
1,503!
622
                : VoidType.instance;
623
            const compatibilityData: TypeCompatibilityData = {};
390✔
624

625
            // `return` statement by itself in non-built-in function will actually result in `invalid`
626
            const valueReturnType = isVoidType(actualReturnType) ? InvalidType.instance : actualReturnType;
390✔
627

628
            if (funcType.returnType.isResolvable()) {
390✔
629
                if (!returnStmt?.value && isVoidType(funcType.returnType)) {
386!
630
                    // allow empty return when function is return `as void`
631
                    // eslint-disable-next-line no-useless-return
632
                    return;
8✔
633
                } else if (!funcType.returnType.isTypeCompatible(valueReturnType, compatibilityData)) {
378✔
634
                    this.addMultiScopeDiagnostic({
28✔
635
                        ...DiagnosticMessages.returnTypeMismatch(actualReturnType.toString(), funcType.returnType.toString(), compatibilityData),
636
                        location: returnStmt.value?.location ?? returnStmt.location
168✔
637
                    });
638
                }
639
            }
640
        }
641
    }
642

643
    /**
644
     * Detect assigned type different from expected member type
645
     */
646
    private validateDottedSetStatement(file: BrsFile, dottedSetStmt: DottedSetStatement) {
647
        const typeChainExpectedLHS = [] as TypeChainEntry[];
101✔
648
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
101✔
649

650
        const expectedLHSType = this.getNodeTypeWrapper(file, dottedSetStmt, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
101✔
651
        const actualRHSType = this.getNodeTypeWrapper(file, dottedSetStmt?.value, getTypeOpts);
101!
652
        const compatibilityData: TypeCompatibilityData = {};
101✔
653
        const typeChainScan = util.processTypeChain(typeChainExpectedLHS);
101✔
654
        // check if anything in typeChain is an AA - if so, just allow it
655
        if (typeChainExpectedLHS.find(typeChainItem => isAssociativeArrayType(typeChainItem.type))) {
177✔
656
            // something in the chain is an AA
657
            // treat members as dynamic - they could have been set without the type system's knowledge
658
            return;
39✔
659
        }
660
        if (!expectedLHSType || !expectedLHSType?.isResolvable()) {
62!
661
            this.addMultiScopeDiagnostic({
5✔
662
                ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
663
                location: typeChainScan?.location
15!
664
            });
665
            return;
5✔
666
        }
667

668
        let accessibilityIsOk = this.checkMemberAccessibility(file, dottedSetStmt, typeChainExpectedLHS);
57✔
669

670
        //Most Component fields can be set with strings
671
        //TODO: be more precise about which fields can actually accept strings
672
        //TODO: if RHS is a string literal, we can do more validation to make sure it's the correct type
673
        if (isComponentType(dottedSetStmt.obj?.getType({ flags: SymbolTypeFlag.runtime }))) {
57!
674
            if (isStringTypeLike(actualRHSType)) {
21✔
675
                return;
6✔
676
            }
677
        }
678

679
        if (accessibilityIsOk && !expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
51!
680
            this.addMultiScopeDiagnostic({
12✔
681
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType?.toString() ?? 'unknown', expectedLHSType?.toString() ?? 'unknown', compatibilityData),
144!
682
                location: dottedSetStmt.location
683
            });
684
        }
685
    }
686

687
    /**
688
     * Detect when declared type does not match rhs type
689
     */
690
    private validateAssignmentStatement(file: BrsFile, assignStmt: AssignmentStatement) {
691
        if (!assignStmt?.typeExpression) {
705!
692
            // nothing to check
693
            return;
697✔
694
        }
695

696
        const typeChainExpectedLHS = [];
8✔
697
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
8✔
698
        const expectedLHSType = this.getNodeTypeWrapper(file, assignStmt.typeExpression, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
8✔
699
        const actualRHSType = this.getNodeTypeWrapper(file, assignStmt.value, getTypeOpts);
8✔
700
        const compatibilityData: TypeCompatibilityData = {};
8✔
701
        if (!expectedLHSType || !expectedLHSType.isResolvable()) {
8✔
702
            // LHS is not resolvable... handled elsewhere
703
        } else if (!expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
7!
704
            this.addMultiScopeDiagnostic({
1✔
705
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
706
                location: assignStmt.location
707
            });
708
        }
709
    }
710

711
    /**
712
     * Detect invalid use of a binary operator
713
     */
714
    private validateBinaryExpression(file: BrsFile, binaryExpr: BinaryExpression | AugmentedAssignmentStatement) {
715
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
342✔
716

717
        if (util.isInTypeExpression(binaryExpr)) {
342✔
718
            return;
13✔
719
        }
720

721
        let leftType = isBinaryExpression(binaryExpr)
329✔
722
            ? this.getNodeTypeWrapper(file, binaryExpr.left, getTypeOpts)
329✔
723
            : this.getNodeTypeWrapper(file, binaryExpr.item, getTypeOpts);
724
        let rightType = isBinaryExpression(binaryExpr)
329✔
725
            ? this.getNodeTypeWrapper(file, binaryExpr.right, getTypeOpts)
329✔
726
            : this.getNodeTypeWrapper(file, binaryExpr.value, getTypeOpts);
727

728
        if (!leftType || !rightType || !leftType.isResolvable() || !rightType.isResolvable()) {
329✔
729
            // Can not find the type. error handled elsewhere
730
            return;
13✔
731
        }
732

733
        let leftTypeToTest = leftType;
316✔
734
        let rightTypeToTest = rightType;
316✔
735

736
        if (isEnumMemberType(leftType) || isEnumType(leftType)) {
316✔
737
            leftTypeToTest = leftType.underlyingType;
11✔
738
        }
739
        if (isEnumMemberType(rightType) || isEnumType(rightType)) {
316✔
740
            rightTypeToTest = rightType.underlyingType;
10✔
741
        }
742

743
        if (isUnionType(leftType) || isUnionType(rightType)) {
316✔
744
            // TODO: it is possible to validate based on innerTypes, but more complicated
745
            // Because you need to verify each combination of types
746
            return;
26✔
747
        }
748
        const opResult = util.binaryOperatorResultType(leftTypeToTest, binaryExpr.tokens.operator, rightTypeToTest);
290✔
749

750
        if (!opResult) {
290✔
751
            // if the result was dynamic or void, that means there wasn't a valid operation
752
            this.addMultiScopeDiagnostic({
9✔
753
                ...DiagnosticMessages.operatorTypeMismatch(binaryExpr.tokens.operator.text, leftType.toString(), rightType.toString()),
754
                location: binaryExpr.location
755
            });
756
        }
757
    }
758

759
    /**
760
     * Detect invalid use of a Unary operator
761
     */
762
    private validateUnaryExpression(file: BrsFile, unaryExpr: UnaryExpression) {
763
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
33✔
764

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

767
        if (!rightType.isResolvable()) {
33!
768
            // Can not find the type. error handled elsewhere
NEW
769
            return;
×
770
        }
771
        let rightTypeToTest = rightType;
33✔
772
        if (isEnumMemberType(rightType)) {
33!
NEW
773
            rightTypeToTest = rightType.underlyingType;
×
774
        }
775

776
        if (isUnionType(rightTypeToTest)) {
33✔
777
            // TODO: it is possible to validate based on innerTypes, but more complicated
778
            // Because you need to verify each combination of types
779

780
        } else if (isDynamicType(rightTypeToTest) || isObjectType(rightTypeToTest)) {
32✔
781
            // operand is basically "any" type... ignore;
782

783
        } else if (isPrimitiveType(rightType)) {
29!
784
            const opResult = util.unaryOperatorResultType(unaryExpr.tokens.operator, rightTypeToTest);
29✔
785
            if (!opResult) {
29✔
786
                this.addMultiScopeDiagnostic({
1✔
787
                    ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
788
                    location: unaryExpr.location
789
                });
790
            }
791
        } else {
792
            // rhs is not a primitive, so no binary operator is allowed
NEW
793
            this.addMultiScopeDiagnostic({
×
794
                ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
795
                location: unaryExpr.location
796
            });
797
        }
798
    }
799

800
    private validateIncrementStatement(file: BrsFile, incStmt: IncrementStatement) {
801
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
11✔
802

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

805
        if (!rightType.isResolvable()) {
11!
806
            // Can not find the type. error handled elsewhere
NEW
807
            return;
×
808
        }
809

810
        if (isUnionType(rightType)) {
11✔
811
            // TODO: it is possible to validate based on innerTypes, but more complicated
812
            // because you need to verify each combination of types
813
        } else if (isDynamicType(rightType) || isObjectType(rightType)) {
9✔
814
            // operand is basically "any" type... ignore
815
        } else if (isNumberType(rightType)) {
8✔
816
            // operand is a number.. this is ok
817
        } else {
818
            // rhs is not a number, so no increment operator is not allowed
819
            this.addMultiScopeDiagnostic({
1✔
820
                ...DiagnosticMessages.operatorTypeMismatch(incStmt.tokens.operator.text, rightType.toString()),
821
                location: incStmt.location
822
            });
823
        }
824
    }
825

826

827
    validateVariableAndDottedGetExpressions(file: BrsFile, expression: VariableExpression | DottedGetExpression) {
828
        if (isDottedGetExpression(expression.parent)) {
5,606✔
829
            // We validate dottedGetExpressions at the top-most level
830
            return;
1,463✔
831
        }
832
        if (isVariableExpression(expression)) {
4,143✔
833
            if (isAssignmentStatement(expression.parent) && expression.parent.tokens.name === expression.tokens.name) {
3,003!
834
                // Don't validate LHS of assignments
NEW
835
                return;
×
836
            } else if (isNamespaceStatement(expression.parent)) {
3,003✔
837
                return;
3✔
838
            }
839
        }
840

841
        let symbolType = SymbolTypeFlag.runtime;
4,140✔
842
        let oppositeSymbolType = SymbolTypeFlag.typetime;
4,140✔
843
        const isUsedAsType = util.isInTypeExpression(expression);
4,140✔
844
        if (isUsedAsType) {
4,140✔
845
            // This is used in a TypeExpression - only look up types from SymbolTable
846
            symbolType = SymbolTypeFlag.typetime;
1,312✔
847
            oppositeSymbolType = SymbolTypeFlag.runtime;
1,312✔
848
        }
849

850
        // Do a complete type check on all DottedGet and Variable expressions
851
        // this will create a diagnostic if an invalid member is accessed
852
        const typeChain: TypeChainEntry[] = [];
4,140✔
853
        const typeData = {} as ExtraSymbolData;
4,140✔
854
        let exprType = this.getNodeTypeWrapper(file, expression, {
4,140✔
855
            flags: symbolType,
856
            typeChain: typeChain,
857
            data: typeData
858
        });
859

860
        const hasValidDeclaration = this.hasValidDeclaration(expression, exprType, typeData?.definingNode);
4,140!
861

862
        //include a hint diagnostic if this type is marked as deprecated
863
        if (typeData.flags & SymbolTypeFlag.deprecated) { // eslint-disable-line no-bitwise
4,140✔
864
            this.addMultiScopeDiagnostic({
2✔
865
                ...DiagnosticMessages.itemIsDeprecated(),
866
                location: expression.tokens.name.location,
867
                tags: [DiagnosticTag.Deprecated]
868
            });
869
        }
870

871
        if (!this.isTypeKnown(exprType) && !hasValidDeclaration) {
4,140✔
872
            if (this.getNodeTypeWrapper(file, expression, { flags: oppositeSymbolType, isExistenceTest: true })?.isResolvable()) {
259!
873
                const oppoSiteTypeChain = [];
5✔
874
                const invalidlyUsedResolvedType = this.getNodeTypeWrapper(file, expression, { flags: oppositeSymbolType, typeChain: oppoSiteTypeChain, isExistenceTest: true });
5✔
875
                const typeChainScan = util.processTypeChain(oppoSiteTypeChain);
5✔
876
                if (isUsedAsType) {
5✔
877
                    this.addMultiScopeDiagnostic({
2✔
878
                        ...DiagnosticMessages.itemCannotBeUsedAsType(typeChainScan.fullChainName),
879
                        location: expression.location
880
                    });
881
                } else if (invalidlyUsedResolvedType && !isReferenceType(invalidlyUsedResolvedType)) {
3✔
882
                    if (!isAliasStatement(expression.parent)) {
1!
883
                        // alias rhs CAN be a type!
NEW
884
                        this.addMultiScopeDiagnostic({
×
885
                            ...DiagnosticMessages.itemCannotBeUsedAsVariable(invalidlyUsedResolvedType.toString()),
886
                            location: expression.location
887
                        });
888
                    }
889
                } else {
890
                    const typeChainScan = util.processTypeChain(typeChain);
2✔
891
                    //if this is a function call, provide a different diagnostic code
892
                    if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
2✔
893
                        this.addMultiScopeDiagnostic({
1✔
894
                            ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
895
                            location: typeChainScan?.location
3!
896
                        });
897
                    } else {
898
                        this.addMultiScopeDiagnostic({
1✔
899
                            ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
900
                            location: typeChainScan?.location
3!
901
                        });
902
                    }
903
                }
904

905
            } else if (!(typeData?.isFromDocComment)) {
254!
906
                // only show "cannot find... " errors if the type is not defined from a doc comment
907
                const typeChainScan = util.processTypeChain(typeChain);
252✔
908
                if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
252✔
909
                    this.addMultiScopeDiagnostic({
27✔
910
                        ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
911
                        location: typeChainScan?.location
81!
912
                    });
913
                } else {
914
                    this.addMultiScopeDiagnostic({
225✔
915
                        ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
916
                        location: typeChainScan?.location
675!
917
                    });
918
                }
919

920
            }
921
        }
922
        if (isUsedAsType) {
4,140✔
923
            return;
1,312✔
924
        }
925

926
        const containingNamespace = expression.findAncestor<NamespaceStatement>(isNamespaceStatement);
2,828✔
927
        const containingNamespaceName = containingNamespace?.getName(ParseMode.BrighterScript);
2,828✔
928

929
        if (!(isCallExpression(expression.parent) && isNewExpression(expression.parent?.parent))) {
2,828!
930
            const classUsedAsVarEntry = this.checkTypeChainForClassUsedAsVar(typeChain, containingNamespaceName);
2,707✔
931
            const isClassInNamespace = containingNamespace?.getSymbolTable().hasSymbol(typeChain[0].name, SymbolTypeFlag.runtime);
2,707✔
932
            if (classUsedAsVarEntry && !isClassInNamespace) {
2,707!
933

NEW
934
                this.addMultiScopeDiagnostic({
×
935
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable(classUsedAsVarEntry.toString()),
936
                    location: expression.location
937
                });
NEW
938
                return;
×
939
            }
940
        }
941

942
        const lastTypeInfo = typeChain[typeChain.length - 1];
2,828✔
943
        const parentTypeInfo = typeChain[typeChain.length - 2];
2,828✔
944

945
        this.checkMemberAccessibility(file, expression, typeChain);
2,828✔
946

947
        if (isNamespaceType(exprType) && !isAliasStatement(expression.parent)) {
2,828✔
948
            this.addMultiScopeDiagnostic({
24✔
949
                ...DiagnosticMessages.itemCannotBeUsedAsVariable('namespace'),
950
                location: expression.location
951
            });
952
        } else if (isEnumType(exprType) && !isAliasStatement(expression.parent)) {
2,804✔
953
            const enumStatement = this.event.scope.getEnum(util.getAllDottedGetPartsAsString(expression));
27✔
954
            if (enumStatement) {
27✔
955
                // there's an enum with this name
956
                this.addMultiScopeDiagnostic({
4✔
957
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable('enum'),
958
                    location: expression.location
959
                });
960
            }
961
        } else if (isDynamicType(exprType) && isEnumType(parentTypeInfo?.type) && isDottedGetExpression(expression)) {
2,777✔
962
            const enumFileLink = this.event.scope.getEnumFileLink(util.getAllDottedGetPartsAsString(expression.obj));
12✔
963
            const typeChainScanForItem = util.processTypeChain(typeChain);
12✔
964
            const typeChainScanForParent = util.processTypeChain(typeChain.slice(0, -1));
12✔
965
            if (enumFileLink) {
12✔
966
                this.addMultiScopeDiagnostic({
8✔
967
                    ...DiagnosticMessages.cannotFindName(lastTypeInfo?.name, typeChainScanForItem.fullChainName, typeChainScanForParent.fullNameOfItem, 'enum'),
24!
968
                    location: lastTypeInfo?.location,
24!
969
                    relatedInformation: [{
970
                        message: 'Enum declared here',
971
                        location: util.createLocationFromRange(
972
                            util.pathToUri(enumFileLink?.file.srcPath),
24!
973
                            enumFileLink?.item?.tokens.name.location?.range
72!
974
                        )
975
                    }]
976
                });
977
            }
978
        }
979
    }
980

981
    private checkTypeChainForClassUsedAsVar(typeChain: TypeChainEntry[], containingNamespaceName: string) {
982
        const ignoreKinds = [AstNodeKind.TypecastExpression, AstNodeKind.NewExpression];
2,707✔
983
        let lowerNameSoFar = '';
2,707✔
984
        let classUsedAsVar;
985
        let isFirst = true;
2,707✔
986
        for (let i = 0; i < typeChain.length - 1; i++) { // do not look at final entry - we CAN use the constructor as a variable
2,707✔
987
            const tce = typeChain[i];
1,367✔
988
            lowerNameSoFar += `${lowerNameSoFar ? '.' : ''}${tce.name.toLowerCase()}`;
1,367✔
989
            if (!isNamespaceType(tce.type)) {
1,367✔
990
                if (isFirst && containingNamespaceName) {
682✔
991
                    lowerNameSoFar = `${containingNamespaceName.toLowerCase()}.${lowerNameSoFar}`;
77✔
992
                }
993
                if (!tce.astNode || ignoreKinds.includes(tce.astNode.kind)) {
682✔
994
                    break;
15✔
995
                } else if (isClassType(tce.type) && lowerNameSoFar.toLowerCase() === tce.type.name.toLowerCase()) {
667✔
996
                    classUsedAsVar = tce.type;
1✔
997
                }
998
                break;
667✔
999
            }
1000
            isFirst = false;
685✔
1001
        }
1002

1003
        return classUsedAsVar;
2,707✔
1004
    }
1005

1006
    /**
1007
     * Adds diagnostics for accibility mismatches
1008
     *
1009
     * @param file file
1010
     * @param expression containing expression
1011
     * @param typeChain type chain to check
1012
     * @returns true if member accesiibility is okay
1013
     */
1014
    private checkMemberAccessibility(file: BscFile, expression: Expression, typeChain: TypeChainEntry[]) {
1015
        for (let i = 0; i < typeChain.length - 1; i++) {
2,885✔
1016
            const parentChainItem = typeChain[i];
1,548✔
1017
            const childChainItem = typeChain[i + 1];
1,548✔
1018
            if (isClassType(parentChainItem.type)) {
1,548✔
1019
                const containingClassStmt = expression.findAncestor<ClassStatement>(isClassStatement);
159✔
1020
                const classStmtThatDefinesChildMember = childChainItem.data?.definingNode?.findAncestor<ClassStatement>(isClassStatement);
159!
1021
                if (classStmtThatDefinesChildMember) {
159✔
1022
                    const definingClassName = classStmtThatDefinesChildMember.getName(ParseMode.BrighterScript);
157✔
1023
                    const inMatchingClassStmt = containingClassStmt?.getName(ParseMode.BrighterScript).toLowerCase() === parentChainItem.type.name.toLowerCase();
157✔
1024
                    // eslint-disable-next-line no-bitwise
1025
                    if (childChainItem.data.flags & SymbolTypeFlag.private) {
157✔
1026
                        if (!inMatchingClassStmt || childChainItem.data.memberOfAncestor) {
16✔
1027
                            this.addMultiScopeDiagnostic({
4✔
1028
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
1029
                                location: expression.location
1030
                            });
1031
                            // there's an error... don't worry about the rest of the chain
1032
                            return false;
4✔
1033
                        }
1034
                    }
1035

1036
                    // eslint-disable-next-line no-bitwise
1037
                    if (childChainItem.data.flags & SymbolTypeFlag.protected) {
153✔
1038
                        const containingClassName = containingClassStmt?.getName(ParseMode.BrighterScript);
13✔
1039
                        const containingNamespaceName = expression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
13✔
1040
                        const ancestorClasses = this.event.scope.getClassHierarchy(containingClassName, containingNamespaceName).map(link => link.item);
13✔
1041
                        const isSubClassOfDefiningClass = ancestorClasses.includes(classStmtThatDefinesChildMember);
13✔
1042

1043
                        if (!isSubClassOfDefiningClass) {
13✔
1044
                            this.addMultiScopeDiagnostic({
5✔
1045
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
1046
                                location: expression.location
1047
                            });
1048
                            // there's an error... don't worry about the rest of the chain
1049
                            return false;
5✔
1050
                        }
1051
                    }
1052
                }
1053

1054
            }
1055
        }
1056
        return true;
2,876✔
1057
    }
1058

1059
    /**
1060
     * Find all "new" statements in the program,
1061
     * and make sure we can find a class with that name
1062
     */
1063
    private validateNewExpression(file: BrsFile, newExpression: NewExpression) {
1064
        const newExprType = this.getNodeTypeWrapper(file, newExpression, { flags: SymbolTypeFlag.typetime });
121✔
1065
        if (isClassType(newExprType)) {
121✔
1066
            return;
113✔
1067
        }
1068

1069
        let potentialClassName = newExpression.className.getName(ParseMode.BrighterScript);
8✔
1070
        const namespaceName = newExpression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
8!
1071
        let newableClass = this.event.scope.getClass(potentialClassName, namespaceName);
8✔
1072

1073
        if (!newableClass) {
8!
1074
            //try and find functions with this name.
1075
            let fullName = util.getFullyQualifiedClassName(potentialClassName, namespaceName);
8✔
1076

1077
            this.addMultiScopeDiagnostic({
8✔
1078
                ...DiagnosticMessages.expressionIsNotConstructable(fullName),
1079
                location: newExpression.className.location
1080
            });
1081

1082
        }
1083
    }
1084

1085
    private validateFunctionExpressionForReturn(func: FunctionExpression) {
1086
        const returnType = func?.returnTypeExpression?.getType({ flags: SymbolTypeFlag.typetime });
2,170!
1087

1088
        if (!returnType || !returnType.isResolvable() || isVoidType(returnType) || isDynamicType(returnType)) {
2,170✔
1089
            return;
1,927✔
1090
        }
1091
        const returns = func.body?.findChild<ReturnStatement>(isReturnStatement, { walkMode: WalkMode.visitAll });
243!
1092
        if (!returns && isStringTypeLike(returnType)) {
243✔
1093
            this.addMultiScopeDiagnostic({
5✔
1094
                ...DiagnosticMessages.returnTypeCoercionMismatch(returnType.toString()),
1095
                location: func.location
1096
            });
1097
        }
1098
    }
1099

1100
    /**
1101
     * Create diagnostics for any duplicate function declarations
1102
     */
1103
    private flagDuplicateFunctionDeclarations() {
1104
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration });
1,891✔
1105

1106
        //for each list of callables with the same name
1107
        for (let [lowerName, callableContainers] of this.event.scope.getCallableContainerMap()) {
1,891✔
1108

1109
            let globalCallables = [] as CallableContainer[];
141,985✔
1110
            let nonGlobalCallables = [] as CallableContainer[];
141,985✔
1111
            let ownCallables = [] as CallableContainer[];
141,985✔
1112
            let ancestorNonGlobalCallables = [] as CallableContainer[];
141,985✔
1113

1114

1115
            for (let container of callableContainers) {
141,985✔
1116
                if (container.scope === this.event.program.globalScope) {
149,583✔
1117
                    globalCallables.push(container);
147,498✔
1118
                } else {
1119
                    nonGlobalCallables.push(container);
2,085✔
1120
                    if (container.scope === this.event.scope) {
2,085✔
1121
                        ownCallables.push(container);
2,055✔
1122
                    } else {
1123
                        ancestorNonGlobalCallables.push(container);
30✔
1124
                    }
1125
                }
1126
            }
1127

1128
            //add info diagnostics about child shadowing parent functions
1129
            if (ownCallables.length > 0 && ancestorNonGlobalCallables.length > 0) {
141,985✔
1130
                for (let container of ownCallables) {
24✔
1131
                    //skip the init function (because every component will have one of those){
1132
                    if (lowerName !== 'init') {
24✔
1133
                        let shadowedCallable = ancestorNonGlobalCallables[ancestorNonGlobalCallables.length - 1];
23✔
1134
                        if (!!shadowedCallable && shadowedCallable.callable.file === container.callable.file) {
23✔
1135
                            //same file: skip redundant imports
1136
                            continue;
20✔
1137
                        }
1138
                        this.addMultiScopeDiagnostic({
3✔
1139
                            ...DiagnosticMessages.overridesAncestorFunction(
1140
                                container.callable.name,
1141
                                container.scope.name,
1142
                                shadowedCallable.callable.file.destPath,
1143
                                //grab the last item in the list, which should be the closest ancestor's version
1144
                                shadowedCallable.scope.name
1145
                            ),
1146
                            location: util.createLocationFromFileRange(container.callable.file, container.callable.nameRange)
1147
                        }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
1148
                    }
1149
                }
1150
            }
1151

1152
            //add error diagnostics about duplicate functions in the same scope
1153
            if (ownCallables.length > 1) {
141,985✔
1154

1155
                for (let callableContainer of ownCallables) {
5✔
1156
                    let callable = callableContainer.callable;
10✔
1157
                    const related = [];
10✔
1158
                    for (const ownCallable of ownCallables) {
10✔
1159
                        const thatNameRange = ownCallable.callable.nameRange;
20✔
1160
                        if (ownCallable.callable.nameRange !== callable.nameRange) {
20✔
1161
                            related.push({
10✔
1162
                                message: `Function declared here`,
1163
                                location: util.createLocationFromRange(
1164
                                    util.pathToUri(ownCallable.callable.file?.srcPath),
30!
1165
                                    thatNameRange
1166
                                )
1167
                            });
1168
                        }
1169
                    }
1170

1171
                    this.addMultiScopeDiagnostic({
10✔
1172
                        ...DiagnosticMessages.duplicateFunctionImplementation(callable.name),
1173
                        location: util.createLocationFromFileRange(callable.file, callable.nameRange),
1174
                        relatedInformation: related
1175
                    }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
1176
                }
1177
            }
1178
        }
1179
    }
1180

1181
    /**
1182
     * Verify that all of the scripts imported by each file in this scope actually exist, and have the correct case
1183
     */
1184
    private validateScriptImportPaths() {
1185
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Imports });
1,891✔
1186

1187
        let scriptImports = this.event.scope.getOwnScriptImports();
1,891✔
1188
        //verify every script import
1189
        for (let scriptImport of scriptImports) {
1,891✔
1190
            let referencedFile = this.event.scope.getFileByRelativePath(scriptImport.destPath);
587✔
1191
            //if we can't find the file
1192
            if (!referencedFile) {
587✔
1193
                //skip the default bslib file, it will exist at transpile time but should not show up in the program during validation cycle
1194
                if (scriptImport.destPath === this.event.program.bslibPkgPath) {
17✔
1195
                    continue;
2✔
1196
                }
1197
                let dInfo: DiagnosticInfo;
1198
                if (scriptImport.text.trim().length === 0) {
15✔
1199
                    dInfo = DiagnosticMessages.scriptSrcCannotBeEmpty();
1✔
1200
                } else {
1201
                    dInfo = DiagnosticMessages.referencedFileDoesNotExist();
14✔
1202
                }
1203

1204
                this.addMultiScopeDiagnostic({
15✔
1205
                    ...dInfo,
1206
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
1207
                }, ScopeValidatorDiagnosticTag.Imports);
1208
                //if the character casing of the script import path does not match that of the actual path
1209
            } else if (scriptImport.destPath !== referencedFile.destPath) {
570✔
1210
                this.addMultiScopeDiagnostic({
2✔
1211
                    ...DiagnosticMessages.scriptImportCaseMismatch(referencedFile.destPath),
1212
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
1213
                }, ScopeValidatorDiagnosticTag.Imports);
1214
            }
1215
        }
1216
    }
1217

1218
    /**
1219
     * Validate all classes defined in this scope
1220
     */
1221
    private validateClasses() {
1222
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Classes });
1,891✔
1223

1224
        let validator = new BsClassValidator(this.event.scope);
1,891✔
1225
        validator.validate();
1,891✔
1226
        for (const diagnostic of validator.diagnostics) {
1,891✔
1227
            this.addMultiScopeDiagnostic({
29✔
1228
                ...diagnostic
1229
            }, ScopeValidatorDiagnosticTag.Classes);
1230
        }
1231
    }
1232

1233

1234
    /**
1235
     * Find various function collisions
1236
     */
1237
    private diagnosticDetectFunctionCollisions(file: BrsFile) {
1238
        const fileUri = util.pathToUri(file.srcPath);
2,226✔
1239
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, tag: ScopeValidatorDiagnosticTag.FunctionCollisions });
2,226✔
1240
        for (let func of file.callables) {
2,226✔
1241
            const funcName = func.getName(ParseMode.BrighterScript);
2,014✔
1242
            const lowerFuncName = funcName?.toLowerCase();
2,014!
1243
            if (lowerFuncName) {
2,014!
1244

1245
                //find function declarations with the same name as a stdlib function
1246
                if (globalCallableMap.has(lowerFuncName)) {
2,014✔
1247
                    this.addMultiScopeDiagnostic({
5✔
1248
                        ...DiagnosticMessages.scopeFunctionShadowedByBuiltInFunction(),
1249
                        location: util.createLocationFromRange(fileUri, func.nameRange)
1250

1251
                    });
1252
                }
1253
            }
1254
        }
1255
    }
1256

1257
    public detectShadowedLocalVar(file: BrsFile, varDeclaration: { expr: AstNode; name: string; type: BscType; nameRange: Range }) {
1258
        const varName = varDeclaration.name;
1,885✔
1259
        const lowerVarName = varName.toLowerCase();
1,885✔
1260
        const callableContainerMap = this.event.scope.getCallableContainerMap();
1,885✔
1261
        const containingNamespace = varDeclaration.expr?.findAncestor<NamespaceStatement>(isNamespaceStatement);
1,885!
1262
        const localVarIsInNamespace = util.isVariableMemberOfNamespace(varDeclaration.name, varDeclaration.expr, containingNamespace);
1,885✔
1263

1264
        const varIsFunction = () => {
1,885✔
1265
            return isCallableType(varDeclaration.type);
11✔
1266
        };
1267

1268
        if (
1,885✔
1269
            //has same name as stdlib
1270
            globalCallableMap.has(lowerVarName)
1271
        ) {
1272
            //local var function with same name as stdlib function
1273
            if (varIsFunction()) {
8✔
1274
                this.addMultiScopeDiagnostic({
1✔
1275
                    ...DiagnosticMessages.localVarFunctionShadowsParentFunction('stdlib'),
1276
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange)
1277
                });
1278
            }
1279
        } else if (callableContainerMap.has(lowerVarName) && !localVarIsInNamespace) {
1,877✔
1280
            const callable = callableContainerMap.get(lowerVarName);
3✔
1281
            //is same name as a callable
1282
            if (varIsFunction()) {
3✔
1283
                this.addMultiScopeDiagnostic({
1✔
1284
                    ...DiagnosticMessages.localVarFunctionShadowsParentFunction('scope'),
1285
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1286
                    relatedInformation: [{
1287
                        message: 'Function declared here',
1288
                        location: util.createLocationFromFileRange(
1289
                            callable[0].callable.file,
1290
                            callable[0].callable.nameRange
1291
                        )
1292
                    }]
1293
                });
1294
            } else {
1295
                this.addMultiScopeDiagnostic({
2✔
1296
                    ...DiagnosticMessages.localVarShadowedByScopedFunction(),
1297
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1298
                    relatedInformation: [{
1299
                        message: 'Function declared here',
1300
                        location: util.createLocationFromRange(
1301
                            util.pathToUri(callable[0].callable.file.srcPath),
1302
                            callable[0].callable.nameRange
1303
                        )
1304
                    }]
1305
                });
1306
            }
1307
            //has the same name as an in-scope class
1308
        } else if (!localVarIsInNamespace) {
1,874✔
1309
            const classStmtLink = this.event.scope.getClassFileLink(lowerVarName);
1,873✔
1310
            if (classStmtLink) {
1,873✔
1311
                this.addMultiScopeDiagnostic({
3✔
1312
                    ...DiagnosticMessages.localVarShadowedByScopedFunction(),
1313
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1314
                    relatedInformation: [{
1315
                        message: 'Class declared here',
1316
                        location: util.createLocationFromRange(
1317
                            util.pathToUri(classStmtLink.file.srcPath),
1318
                            classStmtLink?.item.tokens.name.location?.range
18!
1319
                        )
1320
                    }]
1321
                });
1322
            }
1323
        }
1324
    }
1325

1326
    private validateXmlInterface(scope: XmlScope) {
1327
        if (!scope.xmlFile.parser.ast?.componentElement?.interfaceElement) {
522!
1328
            return;
448✔
1329
        }
1330
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLInterface });
74!
1331

1332
        const iface = scope.xmlFile.parser.ast.componentElement.interfaceElement;
74✔
1333
        const callableContainerMap = scope.getCallableContainerMap();
74✔
1334
        //validate functions
1335
        for (const func of iface.functions) {
74✔
1336
            const name = func.name;
62✔
1337
            if (!name) {
62✔
1338
                this.addDiagnostic({
3✔
1339
                    ...DiagnosticMessages.xmlTagMissingAttribute(func.tokens.startTagName.text, 'name'),
1340
                    location: func.tokens.startTagName.location
1341
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1342
            } else if (!callableContainerMap.has(name.toLowerCase())) {
59✔
1343
                this.addDiagnostic({
4✔
1344
                    ...DiagnosticMessages.xmlFunctionNotFound(name),
1345
                    location: func.getAttribute('name')?.tokens.value.location
12!
1346
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1347
            }
1348
        }
1349
        //validate fields
1350
        for (const field of iface.fields) {
74✔
1351
            const { id, type, onChange } = field;
43✔
1352
            if (!id) {
43✔
1353
                this.addDiagnostic({
3✔
1354
                    ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'id'),
1355
                    location: field.tokens.startTagName.location
1356
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1357
            }
1358
            if (!type) {
43✔
1359
                if (!field.alias) {
3✔
1360
                    this.addDiagnostic({
2✔
1361
                        ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'type'),
1362
                        location: field.tokens.startTagName.location
1363
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1364
                }
1365
            } else if (!SGFieldTypes.includes(type.toLowerCase())) {
40✔
1366
                this.addDiagnostic({
1✔
1367
                    ...DiagnosticMessages.xmlInvalidFieldType(type),
1368
                    location: field.getAttribute('type')?.tokens.value.location
3!
1369
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1370
            }
1371
            if (onChange) {
43✔
1372
                if (!callableContainerMap.has(onChange.toLowerCase())) {
1!
1373
                    this.addDiagnostic({
1✔
1374
                        ...DiagnosticMessages.xmlFunctionNotFound(onChange),
1375
                        location: field.getAttribute('onchange')?.tokens.value.location
3!
1376
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1377
                }
1378
            }
1379
        }
1380
    }
1381

1382
    private validateDocComments(node: AstNode) {
1383
        const doc = brsDocParser.parseNode(node);
251✔
1384
        for (const docTag of doc.tags) {
251✔
1385
            const docTypeTag = docTag as BrsDocWithType;
29✔
1386
            if (!docTypeTag.typeExpression || !docTypeTag.location) {
29✔
1387
                continue;
1✔
1388
            }
1389
            const foundType = docTypeTag.typeExpression?.getType({ flags: SymbolTypeFlag.typetime });
28!
1390
            if (!foundType?.isResolvable()) {
28!
1391
                this.addMultiScopeDiagnostic({
8✔
1392
                    ...DiagnosticMessages.cannotFindName(docTypeTag.typeString),
1393
                    location: brsDocParser.getTypeLocationFromToken(docTypeTag.token) ?? docTypeTag.location
24!
1394
                });
1395
            }
1396
        }
1397
    }
1398

1399
    /**
1400
     * Detect when a child has imported a script that an ancestor also imported
1401
     */
1402
    private diagnosticDetectDuplicateAncestorScriptImports(scope: XmlScope) {
1403
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLImports });
522!
1404
        if (scope.xmlFile.parentComponent) {
522✔
1405
            //build a lookup of pkg paths -> FileReference so we can more easily look up collisions
1406
            let parentScriptImports = scope.xmlFile.getAncestorScriptTagImports();
34✔
1407
            let lookup = {} as Record<string, FileReference>;
34✔
1408
            for (let parentScriptImport of parentScriptImports) {
34✔
1409
                //keep the first occurance of a pkgPath. Parent imports are first in the array
1410
                if (!lookup[parentScriptImport.destPath]) {
30!
1411
                    lookup[parentScriptImport.destPath] = parentScriptImport;
30✔
1412
                }
1413
            }
1414

1415
            //add warning for every script tag that this file shares with an ancestor
1416
            for (let scriptImport of scope.xmlFile.scriptTagImports) {
34✔
1417
                let ancestorScriptImport = lookup[scriptImport.destPath];
30✔
1418
                if (ancestorScriptImport) {
30✔
1419
                    let ancestorComponent = ancestorScriptImport.sourceFile as XmlFile;
21✔
1420
                    let ancestorComponentName = ancestorComponent.componentName?.text ?? ancestorComponent.destPath;
21!
1421
                    this.addDiagnostic({
21✔
1422
                        location: util.createLocationFromFileRange(scope.xmlFile, scriptImport.filePathRange),
1423
                        ...DiagnosticMessages.unnecessaryScriptImportInChildFromParent(ancestorComponentName)
1424
                    }, ScopeValidatorDiagnosticTag.XMLImports);
1425
                }
1426
            }
1427
        }
1428
    }
1429

1430
    /**
1431
     * Wraps the AstNode.getType() method, so that we can do extra processing on the result based on the current file
1432
     * In particular, since BrightScript does not support Unions, and there's no way to cast them to something else
1433
     * if the result of .getType() is a union, and we're in a .brs (brightScript) file, treat the result as Dynamic
1434
     *
1435
     * Also, for BrightScript parse-mode, if .getType() returns a node type, do not validate unknown members.
1436
     *
1437
     * In most cases, this returns the result of node.getType()
1438
     *
1439
     * @param file the current file being processed
1440
     * @param node the node to get the type of
1441
     * @param getTypeOpts any options to pass to node.getType()
1442
     * @returns the processed result type
1443
     */
1444
    private getNodeTypeWrapper(file: BrsFile, node: AstNode, getTypeOpts: GetTypeOptions) {
1445
        const type = node?.getType(getTypeOpts);
9,334!
1446

1447
        if (file.parseMode === ParseMode.BrightScript) {
9,334✔
1448
            // this is a brightscript file
1449
            const typeChain = getTypeOpts.typeChain;
1,050✔
1450
            if (typeChain) {
1,050✔
1451
                const hasUnion = typeChain.reduce((hasUnion, tce) => {
358✔
1452
                    return hasUnion || isUnionType(tce.type);
415✔
1453
                }, false);
1454
                if (hasUnion) {
358✔
1455
                    // there was a union somewhere in the typechain
1456
                    return DynamicType.instance;
6✔
1457
                }
1458
            }
1459
            if (isUnionType(type)) {
1,044✔
1460
                //this is a union
1461
                return DynamicType.instance;
4✔
1462
            }
1463

1464
            if (isComponentType(type)) {
1,040✔
1465
                // modify type to allow any member access for Node types
1466
                type.changeUnknownMemberToDynamic = true;
18✔
1467
            }
1468
        }
1469

1470
        // by default return the result of node.getType()
1471
        return type;
9,324✔
1472
    }
1473

1474
    private getParentTypeDescriptor(typeChainResult: TypeChainProcessResult) {
1475
        if (typeChainResult.itemParentTypeKind === BscTypeKind.NamespaceType) {
259✔
1476
            return 'namespace';
119✔
1477
        }
1478
        return 'type';
140✔
1479
    }
1480

1481
    private addDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1482
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
51!
1483
        this.event.program.diagnostics.register(diagnostic, {
51✔
1484
            tags: [diagnosticTag],
1485
            segment: this.currentSegmentBeingValidated
1486
        });
1487
    }
1488

1489
    /**
1490
     * Add a diagnostic (to the first scope) that will have `relatedInformation` for each affected scope
1491
     */
1492
    private addMultiScopeDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1493
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
532✔
1494
        this.event.program.diagnostics.register(diagnostic, {
532✔
1495
            tags: [diagnosticTag],
1496
            segment: this.currentSegmentBeingValidated,
1497
            scope: this.event.scope
1498
        });
1499
    }
1500
}
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