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

rokucommunity / brighterscript / #14044

20 Mar 2025 07:09PM UTC coverage: 87.163% (-2.0%) from 89.117%
#14044

push

web-flow
Merge e33b1f944 into 0eceb0830

13257 of 16072 branches covered (82.49%)

Branch coverage included in aggregate %.

1163 of 1279 new or added lines in 24 files covered. (90.93%)

802 existing lines in 52 files now uncovered.

14323 of 15570 relevant lines covered (91.99%)

21312.85 hits per line

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

91.81
/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();
96!
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,937✔
72
    private validationKindsMetrics = new Map<string, { timeMs: number; count: number }>();
1,937✔
73

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

91
        logger.time(LogLevel.debug, ['Validating scope', this.event.scope.name], () => {
1,849✔
92
            metrics.fileWalkTime = validationStopwatch.getDurationTextFor(() => {
1,849✔
93
                this.walkFiles();
1,849✔
94
            }).durationText;
95
            this.currentSegmentBeingValidated = null;
1,846✔
96
            metrics.flagDuplicateFunctionTime = validationStopwatch.getDurationTextFor(() => {
1,846✔
97
                this.flagDuplicateFunctionDeclarations();
1,846✔
98
            }).durationText;
99
            metrics.scriptImportValidationTime = validationStopwatch.getDurationTextFor(() => {
1,846✔
100
                this.validateScriptImportPaths();
1,846✔
101
            }).durationText;
102
            metrics.classValidationTime = validationStopwatch.getDurationTextFor(() => {
1,846✔
103
                this.validateClasses();
1,846✔
104
            }).durationText;
105
            metrics.xmlValidationTime = validationStopwatch.getDurationTextFor(() => {
1,846✔
106
                if (isXmlScope(this.event.scope)) {
1,846✔
107
                    //detect when the child imports a script that its ancestor also imports
108
                    this.diagnosticDetectDuplicateAncestorScriptImports(this.event.scope);
520✔
109
                    //validate component interface
110
                    this.validateXmlInterface(this.event.scope);
520✔
111
                }
112
            }).durationText;
113
        });
114
        logger.debug(this.event.scope.name, 'segment metrics:');
1,846✔
115
        let totalSegments = 0;
1,846✔
116
        for (const [filePath, metric] of this.segmentsMetrics) {
1,846✔
117
            this.event.program.logger.debug(' - ', filePath, metric.segments, metric.time);
1,770✔
118
            totalSegments += metric.segments;
1,770✔
119
        }
120
        logger.debug(this.event.scope.name, 'total segments validated', totalSegments);
1,846✔
121
        this.logValidationMetrics(metrics);
1,846✔
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,846✔
127
        for (let key in metrics) {
1,846✔
128
            logs.push(`${key}=${chalk.yellow(metrics[key].toString())}`);
9,230✔
129
        }
130
        this.event.program.logger.debug(`Validation Metrics (Scope: ${this.event.scope.name}): ${logs.join(', ')}`);
1,846✔
131
        let kindsLogs = [] as string[];
1,846✔
132
        const kindsArray = Array.from(this.validationKindsMetrics.keys()).sort();
1,846✔
133
        for (let key of kindsArray) {
1,846✔
134
            const timeData = this.validationKindsMetrics.get(key);
5,898✔
135
            kindsLogs.push(`${key}=${chalk.yellow(timeData.timeMs.toFixed(3).toString()) + 'ms'} (${timeData.count})`);
5,898✔
136
        }
137
        this.event.program.logger.debug(`Validation Walk Metrics (Scope: ${this.event.scope.name}): ${kindsLogs.join(', ')}`);
1,846✔
138
    }
139

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

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

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

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

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

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

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

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

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

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

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

180
                const validationVisitor = createVisitor({
1,773✔
181
                    VariableExpression: (varExpr) => {
182
                        this.addValidationKindMetric('VariableExpression', () => {
3,955✔
183
                            this.validateVariableAndDottedGetExpressions(file, varExpr);
3,955✔
184
                        });
185
                    },
186
                    DottedGetExpression: (dottedGet) => {
187
                        this.addValidationKindMetric('DottedGetExpression', () => {
1,554✔
188
                            this.validateVariableAndDottedGetExpressions(file, dottedGet);
1,554✔
189
                        });
190
                    },
191
                    CallExpression: (functionCall) => {
192
                        this.addValidationKindMetric('CallExpression', () => {
964✔
193
                            this.validateCallExpression(file, functionCall);
964✔
194
                            this.validateCreateObjectCall(file, functionCall);
964✔
195
                            this.validateComponentMethods(file, functionCall);
964✔
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', () => {
363✔
205
                            this.validateReturnStatement(file, returnStatement);
363✔
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', () => {
279✔
215
                            this.validateBinaryExpression(file, binaryExpr);
279✔
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', () => {
692✔
225
                            this.validateAssignmentStatement(file, assignStmt);
692✔
226
                            // Note: this also includes For statements
227
                            this.detectShadowedLocalVar(file, {
692✔
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,076✔
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,142✔
262
                            this.detectShadowedLocalVar(file, {
1,142✔
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,426✔
267
                            });
268
                        });
269
                    },
270
                    FunctionExpression: (func) => {
271
                        this.addValidationKindMetric('FunctionExpression', () => {
2,121✔
272
                            this.validateFunctionExpressionForReturn(func);
2,121✔
273
                        });
274
                    },
275
                    AstNode: (node) => {
276
                        //check for doc comments
277
                        if (!node.leadingTrivia || node.leadingTrivia.filter(triviaToken => triviaToken.kind === TokenKind.Comment).length === 0) {
25,996✔
278
                            return;
19,914✔
279
                        }
280
                        this.addValidationKindMetric('AstNode', () => {
243✔
281
                            this.validateDocComments(node);
243✔
282
                        });
283
                    }
284
                });
285
                // validate only what's needed in the file
286

287
                const segmentsToWalkForValidation = thisFileHasChanges
1,773✔
288
                    ? file.validationSegmenter.getAllUnvalidatedSegments()
1,773✔
289
                    : file.validationSegmenter.getSegmentsWithChangedSymbols(this.event.changedSymbols);
290

291
                let segmentsValidated = 0;
1,773✔
292

293
                if (thisFileHasChanges) {
1,773✔
294
                    // clear all ScopeValidatorSegment diagnostics for this file
295
                    this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, tag: ScopeValidatorDiagnosticTag.Segment });
1,643✔
296
                }
297

298

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

322
    private addValidationKindMetric(name: string, funcToTime: () => void) {
323
        if (!this.validationKindsMetrics.has(name)) {
11,715✔
324
            this.validationKindsMetrics.set(name, { timeMs: 0, count: 0 });
5,910✔
325
        }
326
        const timeData = this.validationKindsMetrics.get(name);
11,715✔
327
        const validationKindStopWatch = new Stopwatch();
11,715✔
328
        validationKindStopWatch.start();
11,715✔
329
        funcToTime();
11,715✔
330
        validationKindStopWatch.stop();
11,712✔
331
        this.validationKindsMetrics.set(name, { timeMs: timeData.timeMs + validationKindStopWatch.totalMilliseconds, count: timeData.count + 1 });
11,712✔
332
    }
333

334
    private doesFileProvideChangedSymbol(file: BrsFile, changedSymbols: Map<SymbolTypeFlag, Set<string>>) {
335
        if (!changedSymbols) {
192!
UNCOV
336
            return true;
×
337
        }
338
        for (const flag of [SymbolTypeFlag.runtime, SymbolTypeFlag.typetime]) {
192✔
339
            const providedSymbolKeysFlag = file.providedSymbols.symbolMap.get(flag).keys();
384✔
340
            const changedSymbolSetForFlag = changedSymbols.get(flag);
384✔
341

342
            for (let providedKey of providedSymbolKeysFlag) {
384✔
343
                if (changedSymbolSetForFlag.has(providedKey)) {
294✔
344
                    return true;
8✔
345
                }
346
            }
347
        }
348
        return false;
184✔
349
    }
350

351
    private currentSegmentBeingValidated: AstNode;
352

353

354
    private isTypeKnown(exprType: BscType) {
355
        let isKnownType = exprType?.isResolvable();
4,060✔
356
        return isKnownType;
4,060✔
357
    }
358

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

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

388
        //skip non CreateObject function calls
389
        const callName = util.getAllDottedGetPartsAsString(call.callee)?.toLowerCase();
964✔
390
        if (callName !== 'createobject' || !isLiteralExpression(call?.args[0])) {
964!
391
            return;
891✔
392
        }
393
        const firstParamToken = (call?.args[0] as LiteralExpression)?.tokens?.value;
73!
394
        const firstParamStringValue = firstParamToken?.text?.replace(/"/g, '');
73!
395
        if (!firstParamStringValue) {
73!
UNCOV
396
            return;
×
397
        }
398
        const firstParamStringValueLower = firstParamStringValue.toLowerCase();
73✔
399

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

434
            // Test for deprecation
435
            if (brightScriptComponent?.isDeprecated) {
35!
UNCOV
436
                this.addDiagnostic({
×
437
                    ...DiagnosticMessages.itemIsDeprecated(firstParamStringValue, brightScriptComponent.deprecatedDescription),
438
                    location: call.location
439
                });
440
            }
441
        }
442

443
    }
444

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

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

469
        const callName = call.callee.tokens?.name?.text?.toLowerCase();
394!
470
        if (!callName || !lowerMethodNamesChecked.includes(callName) || !isLiteralExpression(call?.args[0])) {
394!
471
            return;
381✔
472
        }
473

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

496

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

507
    }
508

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

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

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

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

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

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

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

585
            if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) {
599✔
586
                argType = data.definingNode.getConstructorType();
2✔
587
            }
588

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

601
    private checkAllowedArgConversions(paramType: BscType, argType: BscType): boolean {
602
        if (isNumberType(argType) && isBooleanType(paramType)) {
599✔
603
            return true;
8✔
604
        }
605
        return false;
591✔
606
    }
607

608

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

622
            // `return` statement by itself in non-built-in function will actually result in `invalid`
623
            const valueReturnType = isVoidType(actualReturnType) ? InvalidType.instance : actualReturnType;
363✔
624

625
            if (funcType.returnType.isResolvable()) {
363✔
626
                if (!returnStmt?.value && isVoidType(funcType.returnType)) {
359!
627
                    // allow empty return when function is return `as void`
628
                    // eslint-disable-next-line no-useless-return
629
                    return;
7✔
630
                } else if (!funcType.returnType.isTypeCompatible(valueReturnType, compatibilityData)) {
352✔
631
                    this.addMultiScopeDiagnostic({
14✔
632
                        ...DiagnosticMessages.returnTypeMismatch(actualReturnType.toString(), funcType.returnType.toString(), compatibilityData),
633
                        location: returnStmt.value?.location ?? returnStmt.location
84✔
634
                    });
635
                }
636
            }
637
        }
638
    }
639

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

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

665
        let accessibilityIsOk = this.checkMemberAccessibility(file, dottedSetStmt, typeChainExpectedLHS);
57✔
666

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

676
        if (accessibilityIsOk && !expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
51!
677
            this.addMultiScopeDiagnostic({
12✔
678
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
679
                location: dottedSetStmt.location
680
            });
681
        }
682
    }
683

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

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

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

714
        if (util.isInTypeExpression(binaryExpr)) {
341✔
715
            return;
13✔
716
        }
717

718
        let leftType = isBinaryExpression(binaryExpr)
328✔
719
            ? this.getNodeTypeWrapper(file, binaryExpr.left, getTypeOpts)
328✔
720
            : this.getNodeTypeWrapper(file, binaryExpr.item, getTypeOpts);
721
        let rightType = isBinaryExpression(binaryExpr)
328✔
722
            ? this.getNodeTypeWrapper(file, binaryExpr.right, getTypeOpts)
328✔
723
            : this.getNodeTypeWrapper(file, binaryExpr.value, getTypeOpts);
724

725
        if (!leftType || !rightType || !leftType.isResolvable() || !rightType.isResolvable()) {
328✔
726
            // Can not find the type. error handled elsewhere
727
            return;
13✔
728
        }
729

730
        let leftTypeToTest = leftType;
315✔
731
        let rightTypeToTest = rightType;
315✔
732

733
        if (isEnumMemberType(leftType) || isEnumType(leftType)) {
315✔
734
            leftTypeToTest = leftType.underlyingType;
11✔
735
        }
736
        if (isEnumMemberType(rightType) || isEnumType(rightType)) {
315✔
737
            rightTypeToTest = rightType.underlyingType;
10✔
738
        }
739

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

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

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

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

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

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

777
        } else if (isDynamicType(rightTypeToTest) || isObjectType(rightTypeToTest)) {
32✔
778
            // operand is basically "any" type... ignore;
779

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

797
    private validateIncrementStatement(file: BrsFile, incStmt: IncrementStatement) {
798
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
11✔
799

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

802
        if (!rightType.isResolvable()) {
11!
803
            // Can not find the type. error handled elsewhere
UNCOV
804
            return;
×
805
        }
806

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

823

824
    validateVariableAndDottedGetExpressions(file: BrsFile, expression: VariableExpression | DottedGetExpression) {
825
        if (isDottedGetExpression(expression.parent)) {
5,509✔
826
            // We validate dottedGetExpressions at the top-most level
827
            return;
1,446✔
828
        }
829
        if (isVariableExpression(expression)) {
4,063✔
830
            if (isAssignmentStatement(expression.parent) && expression.parent.tokens.name === expression.tokens.name) {
2,934!
831
                // Don't validate LHS of assignments
UNCOV
832
                return;
×
833
            } else if (isNamespaceStatement(expression.parent)) {
2,934✔
834
                return;
3✔
835
            }
836
        }
837

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

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

857
        const hasValidDeclaration = this.hasValidDeclaration(expression, exprType, typeData?.definingNode);
4,060!
858

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

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

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

917
            }
918
        }
919
        if (isUsedAsType) {
4,060✔
920
            return;
1,277✔
921
        }
922

923
        const containingNamespace = expression.findAncestor<NamespaceStatement>(isNamespaceStatement);
2,783✔
924
        const containingNamespaceName = containingNamespace?.getName(ParseMode.BrighterScript);
2,783✔
925

926
        if (!(isCallExpression(expression.parent) && isNewExpression(expression.parent?.parent))) {
2,783!
927
            const classUsedAsVarEntry = this.checkTypeChainForClassUsedAsVar(typeChain, containingNamespaceName);
2,662✔
928
            const isClassInNamespace = containingNamespace?.getSymbolTable().hasSymbol(typeChain[0].name, SymbolTypeFlag.runtime);
2,662✔
929
            if (classUsedAsVarEntry && !isClassInNamespace) {
2,662!
930

UNCOV
931
                this.addMultiScopeDiagnostic({
×
932
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable(classUsedAsVarEntry.toString()),
933
                    location: expression.location
934
                });
UNCOV
935
                return;
×
936
            }
937
        }
938

939
        const lastTypeInfo = typeChain[typeChain.length - 1];
2,783✔
940
        const parentTypeInfo = typeChain[typeChain.length - 2];
2,783✔
941

942
        this.checkMemberAccessibility(file, expression, typeChain);
2,783✔
943

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

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

1000
        return classUsedAsVar;
2,662✔
1001
    }
1002

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

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

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

1051
            }
1052
        }
1053
        return true;
2,831✔
1054
    }
1055

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

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

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

1074
            this.addMultiScopeDiagnostic({
8✔
1075
                ...DiagnosticMessages.expressionIsNotConstructable(fullName),
1076
                location: newExpression.className.location
1077
            });
1078

1079
        }
1080
    }
1081

1082
    private validateFunctionExpressionForReturn(func: FunctionExpression) {
1083
        const returnType = func?.returnTypeExpression?.getType({ flags: SymbolTypeFlag.typetime });
2,121!
1084

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

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

1103
        //for each list of callables with the same name
1104
        for (let [lowerName, callableContainers] of this.event.scope.getCallableContainerMap()) {
1,846✔
1105

1106
            let globalCallables = [] as CallableContainer[];
138,606✔
1107
            let nonGlobalCallables = [] as CallableContainer[];
138,606✔
1108
            let ownCallables = [] as CallableContainer[];
138,606✔
1109
            let ancestorNonGlobalCallables = [] as CallableContainer[];
138,606✔
1110

1111

1112
            for (let container of callableContainers) {
138,606✔
1113
                if (container.scope === this.event.program.globalScope) {
146,024✔
1114
                    globalCallables.push(container);
143,988✔
1115
                } else {
1116
                    nonGlobalCallables.push(container);
2,036✔
1117
                    if (container.scope === this.event.scope) {
2,036✔
1118
                        ownCallables.push(container);
2,006✔
1119
                    } else {
1120
                        ancestorNonGlobalCallables.push(container);
30✔
1121
                    }
1122
                }
1123
            }
1124

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

1149
            //add error diagnostics about duplicate functions in the same scope
1150
            if (ownCallables.length > 1) {
138,606✔
1151

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

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

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

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

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

1215
    /**
1216
     * Validate all classes defined in this scope
1217
     */
1218
    private validateClasses() {
1219
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Classes });
1,846✔
1220

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

1230

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

1242
                //find function declarations with the same name as a stdlib function
1243
                if (globalCallableMap.has(lowerFuncName)) {
1,839✔
1244
                    this.addMultiScopeDiagnostic({
5✔
1245
                        ...DiagnosticMessages.scopeFunctionShadowedByBuiltInFunction(),
1246
                        location: util.createLocationFromRange(fileUri, func.nameRange)
1247

1248
                    });
1249
                }
1250
            }
1251
        }
1252
    }
1253

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

1261
        const varIsFunction = () => {
1,860✔
1262
            return isCallableType(varDeclaration.type);
11✔
1263
        };
1264

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

1323
    private validateXmlInterface(scope: XmlScope) {
1324
        if (!scope.xmlFile.parser.ast?.componentElement?.interfaceElement) {
520!
1325
            return;
448✔
1326
        }
1327
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLInterface });
72!
1328

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

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

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

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

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

1444
        if (file.parseMode === ParseMode.BrightScript) {
9,158✔
1445
            // this is a brightscript file
1446
            const typeChain = getTypeOpts.typeChain;
1,005✔
1447
            if (typeChain) {
1,005✔
1448
                const hasUnion = typeChain.reduce((hasUnion, tce) => {
341✔
1449
                    return hasUnion || isUnionType(tce.type);
396✔
1450
                }, false);
1451
                if (hasUnion) {
341✔
1452
                    // there was a union somewhere in the typechain
1453
                    return DynamicType.instance;
6✔
1454
                }
1455
            }
1456
            if (isUnionType(type)) {
999✔
1457
                //this is a union
1458
                return DynamicType.instance;
4✔
1459
            }
1460

1461
            if (isComponentType(type)) {
995✔
1462
                // modify type to allow any member access for Node types
1463
                type.changeUnknownMemberToDynamic = true;
18✔
1464
            }
1465
        }
1466

1467
        // by default return the result of node.getType()
1468
        return type;
9,148✔
1469
    }
1470

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

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

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