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

rokucommunity / brighterscript / #15135

28 Jan 2026 04:38PM UTC coverage: 87.198%. Remained the same
#15135

push

web-flow
Merge f28626da6 into 610607efc

14643 of 17747 branches covered (82.51%)

Branch coverage included in aggregate %.

76 of 78 new or added lines in 11 files covered. (97.44%)

200 existing lines in 8 files now uncovered.

15402 of 16709 relevant lines covered (92.18%)

24805.87 hits per line

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

91.07
/src/bscPlugin/validation/ScopeValidator.ts
1
import { DiagnosticTag, type Range } from 'vscode-languageserver';
1✔
2
import { isAliasStatement, isArrayType, isAssignmentStatement, isAssociativeArrayType, isBinaryExpression, isBooleanTypeLike, isBrsFile, isCallExpression, isCallFuncableTypeLike, isCallableType, isCallfuncExpression, isClassStatement, isClassType, isComponentType, isDottedGetExpression, isDynamicType, isEnumMemberType, isEnumType, isFunctionExpression, isFunctionParameterExpression, isIterableType, isLiteralExpression, isNamespaceStatement, isNamespaceType, isNewExpression, isNumberTypeLike, 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, ValidateScopeEvent, TypeChainEntry, TypeChainProcessResult, TypeCompatibilityData } from '../../interfaces';
7
import { SymbolTypeFlag } from '../../SymbolTypeFlag';
1✔
8
import type { AssignmentStatement, AugmentedAssignmentStatement, ClassStatement, DottedSetStatement, ForEachStatement, ForStatement, 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
import { IntegerType } from '../../types/IntegerType';
1✔
40

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

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

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

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

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

75
    public processEvent(event: ValidateScopeEvent) {
76
        this.event = event;
4,260✔
77
        if (this.event.program.globalScope === this.event.scope) {
4,260✔
78
            return;
2,173✔
79
        }
80
        const logger = this.event.program.logger;
2,087✔
81
        const metrics = {
2,087✔
82
            fileWalkTime: '',
83
            flagDuplicateFunctionTime: '',
84
            classValidationTime: '',
85
            scriptImportValidationTime: '',
86
            xmlValidationTime: ''
87
        };
88
        this.segmentsMetrics.clear();
2,087✔
89
        this.validationKindsMetrics.clear();
2,087✔
90
        const validationStopwatch = new Stopwatch();
2,087✔
91

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

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

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

145
    private walkFiles() {
146
        const hasChangeInfo = this.event.changedFiles && this.event.changedSymbols;
2,087✔
147

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

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

156
            if (thisFileHasChanges || this.doesFileProvideChangedSymbol(file, this.event.changedSymbols)) {
2,502✔
157
                this.diagnosticDetectFunctionCollisions(file);
2,439✔
158
            }
159
        });
160
        const fileWalkStopWatch = new Stopwatch();
2,087✔
161

162
        this.event.scope.enumerateOwnFiles((file) => {
2,087✔
163
            if (isBrsFile(file)) {
3,036✔
164

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

169
                fileWalkStopWatch.reset();
2,492✔
170
                fileWalkStopWatch.start();
2,492✔
171

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

175
                const hasUnvalidatedSegments = file.validationSegmenter.hasUnvalidatedSegments();
2,492✔
176

177
                if (hasChangeInfo && !hasUnvalidatedSegments) {
2,492✔
178
                    return;
473✔
179
                }
180

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

297
                const segmentsToWalkForValidation = thisFileHasChanges
2,019✔
298
                    ? file.validationSegmenter.getAllUnvalidatedSegments()
2,019!
299
                    : file.validationSegmenter.getSegmentsWithChangedSymbols(this.event.changedSymbols);
300

301
                let segmentsValidated = 0;
2,019✔
302

303
                if (thisFileHasChanges) {
2,019!
304
                    // clear all ScopeValidatorSegment diagnostics for this file
305
                    this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, tag: ScopeValidatorDiagnosticTag.Segment });
2,019✔
306
                }
307

308

309
                for (const segment of segmentsToWalkForValidation) {
2,019✔
310
                    if (!thisFileHasChanges && !file.validationSegmenter.checkIfSegmentNeedsRevalidation(segment, this.event.changedSymbols)) {
3,911!
UNCOV
311
                        continue;
×
312
                    }
313
                    this.currentSegmentBeingValidated = segment;
3,911✔
314
                    if (!thisFileHasChanges) {
3,911!
315
                        // just clear the affected diagnostics
UNCOV
316
                        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, segment: segment, tag: ScopeValidatorDiagnosticTag.Segment });
×
317
                    }
318
                    segmentsValidated++;
3,911✔
319
                    segment.walk(validationVisitor, {
3,911✔
320
                        walkMode: InsideSegmentWalkMode
321
                    });
322
                    file.markSegmentAsValidated(segment);
3,908✔
323
                    this.currentSegmentBeingValidated = null;
3,908✔
324
                }
325
                fileWalkStopWatch.stop();
2,016✔
326
                const timeString = fileWalkStopWatch.getDurationText();
2,016✔
327
                this.segmentsMetrics.set(file.pkgPath, { segments: segmentsValidated, time: timeString });
2,016✔
328
            }
329
        });
330
    }
331

332
    private addValidationKindMetric(name: string, funcToTime: () => void) {
333
        if (!this.validationKindsMetrics.has(name)) {
13,759✔
334
            this.validationKindsMetrics.set(name, { timeMs: 0, count: 0 });
6,915✔
335
        }
336
        const timeData = this.validationKindsMetrics.get(name);
13,759✔
337
        const validationKindStopWatch = new Stopwatch();
13,759✔
338
        validationKindStopWatch.start();
13,759✔
339
        funcToTime();
13,759✔
340
        validationKindStopWatch.stop();
13,756✔
341
        this.validationKindsMetrics.set(name, { timeMs: timeData.timeMs + validationKindStopWatch.totalMilliseconds, count: timeData.count + 1 });
13,756✔
342
    }
343

344
    private doesFileProvideChangedSymbol(file: BrsFile, changedSymbols: Map<SymbolTypeFlag, Set<string>>) {
345
        if (!changedSymbols) {
64!
UNCOV
346
            return true;
×
347
        }
348
        for (const flag of [SymbolTypeFlag.runtime, SymbolTypeFlag.typetime]) {
64✔
349
            const providedSymbolKeysFlag = file.providedSymbols.symbolMap.get(flag).keys();
128✔
350
            const changedSymbolSetForFlag = changedSymbols.get(flag);
128✔
351

352
            for (let providedKey of providedSymbolKeysFlag) {
128✔
353
                if (changedSymbolSetForFlag.has(providedKey)) {
50✔
354
                    return true;
1✔
355
                }
356
            }
357
        }
358
        return false;
63✔
359
    }
360

361
    private currentSegmentBeingValidated: AstNode;
362

363

364
    private isTypeKnown(exprType: BscType) {
365
        let isKnownType = exprType?.isResolvable();
4,937✔
366
        return isKnownType;
4,937✔
367
    }
368

369
    private getCircularReference(exprType: BscType) {
370
        if (exprType?.isResolvable()) {
264!
UNCOV
371
            return { isCircularReference: false };
×
372
        }
373
        if (isReferenceType(exprType)) {
264✔
374

375
            const info = exprType.getCircularReferenceInfo();
259✔
376
            return info;
259✔
377
        }
378
        return { isCircularReference: false };
5✔
379
    }
380

381

382
    /**
383
     * If this is the lhs of an assignment, we don't need to flag it as unresolved
384
     */
385
    private hasValidDeclaration(expression: Expression, exprType: BscType, definingNode?: AstNode) {
386
        if (!isVariableExpression(expression)) {
4,937✔
387
            return false;
1,242✔
388
        }
389
        let assignmentAncestor: AssignmentStatement;
390
        if (isAssignmentStatement(definingNode) && definingNode.tokens.equals.kind === TokenKind.Equal) {
3,695✔
391
            // this symbol was defined in a "normal" assignment (eg. not a compound assignment)
392
            assignmentAncestor = definingNode;
469✔
393
            return assignmentAncestor?.tokens.name?.text.toLowerCase() === expression?.tokens.name?.text.toLowerCase();
469!
394
        } else if (isFunctionParameterExpression(definingNode)) {
3,226✔
395
            // this symbol was defined in a function param
396
            return true;
661✔
397
        } else {
398
            assignmentAncestor = expression?.findAncestor(isAssignmentStatement);
2,565!
399
        }
400
        return assignmentAncestor?.tokens.name === expression?.tokens.name && isUnionType(exprType);
2,565!
401
    }
402

403
    /**
404
     * Validate every function call to `CreateObject`.
405
     * Ideally we would create better type checking/handling for this, but in the mean time, we know exactly
406
     * what these calls are supposed to look like, and this is a very common thing for brs devs to do, so just
407
     * do this manually for now.
408
     */
409
    protected validateCreateObjectCall(file: BrsFile, call: CallExpression) {
410

411
        //skip non CreateObject function calls
412
        const callName = util.getAllDottedGetPartsAsString(call.callee)?.toLowerCase();
1,074✔
413
        if (callName !== 'createobject' || !isLiteralExpression(call?.args[0])) {
1,074!
414
            return;
994✔
415
        }
416
        const firstParamToken = (call?.args[0] as LiteralExpression)?.tokens?.value;
80!
417
        const firstParamStringValue = firstParamToken?.text?.replace(/"/g, '');
80!
418
        if (!firstParamStringValue) {
80!
UNCOV
419
            return;
×
420
        }
421
        const firstParamStringValueLower = firstParamStringValue.toLowerCase();
80✔
422

423
        //if this is a `createObject('roSGNode'` call, only support known sg node types
424
        if (firstParamStringValueLower === 'rosgnode' && isLiteralExpression(call?.args[1])) {
80!
425
            const componentName: Token = call?.args[1]?.tokens.value;
32!
426
            this.checkComponentName(componentName);
32✔
427
            if (call?.args.length !== 2) {
32!
428
                // roSgNode should only ever have 2 args in `createObject`
429
                this.addDiagnostic({
1✔
430
                    ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, [2], call?.args.length),
3!
431
                    location: call.location
432
                });
433
            }
434
        } else if (!platformComponentNames.has(firstParamStringValueLower)) {
48✔
435
            this.addDiagnostic({
7✔
436
                ...DiagnosticMessages.unknownBrightScriptComponent(firstParamStringValue),
437
                location: firstParamToken.location
438
            });
439
        } else {
440
            // This is valid brightscript component
441
            // Test for invalid arg counts
442
            const brightScriptComponent: BRSComponentData = components[firstParamStringValueLower];
41✔
443
            // Valid arg counts for createObject are 1+ number of args for constructor
444
            let validArgCounts = brightScriptComponent?.constructors.map(cnstr => cnstr.params.length + 1);
41!
445
            if (validArgCounts.length === 0) {
41✔
446
                // no constructors for this component, so createObject only takes 1 arg
447
                validArgCounts = [1];
6✔
448
            }
449
            if (!validArgCounts.includes(call?.args.length)) {
41!
450
                // Incorrect number of arguments included in `createObject()`
451
                this.addDiagnostic({
4✔
452
                    ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, validArgCounts, call?.args.length),
12!
453
                    location: call.location
454
                });
455
            }
456

457
            // Test for deprecation
458
            if (brightScriptComponent?.isDeprecated) {
41!
UNCOV
459
                this.addDiagnostic({
×
460
                    ...DiagnosticMessages.itemIsDeprecated(firstParamStringValue, brightScriptComponent.deprecatedDescription),
461
                    location: call.location
462
                });
463
            }
464
        }
465

466
    }
467

468
    private checkComponentName(componentName: Token) {
469
        //don't validate any components with a colon in their name (probably component libraries, but regular components can have them too).
470
        if (!componentName || componentName?.text?.includes(':')) {
33!
471
            return;
4✔
472
        }
473
        //add diagnostic for unknown components
474
        const unquotedComponentName = componentName?.text?.replace(/"/g, '');
29!
475
        if (unquotedComponentName && !platformNodeNames.has(unquotedComponentName.toLowerCase()) && !this.event.program.getComponent(unquotedComponentName)) {
29✔
476
            this.addDiagnostic({
4✔
477
                ...DiagnosticMessages.unknownRoSGNode(unquotedComponentName),
478
                location: componentName.location
479
            });
480
        }
481
    }
482

483
    /**
484
     * Validate every method call to `component.callfunc()`, `component.createChild()`, etc.
485
     */
486
    protected validateComponentMethods(file: BrsFile, call: CallExpression) {
487
        const lowerMethodNamesChecked = ['callfunc', 'createchild'];
1,074✔
488
        if (!isDottedGetExpression(call.callee)) {
1,074✔
489
            return;
647✔
490
        }
491

492
        const callName = call.callee.tokens?.name?.text?.toLowerCase();
427!
493
        if (!callName || !lowerMethodNamesChecked.includes(callName) || !isLiteralExpression(call?.args[0])) {
427!
494
            return;
414✔
495
        }
496

497
        const callerType = call.callee.obj?.getType({ flags: SymbolTypeFlag.runtime });
13!
498
        if (!isCallFuncableTypeLike(callerType)) {
13✔
499
            return;
2✔
500
        }
501
        const firstArgToken = call?.args[0]?.tokens.value;
11!
502
        if (callName === 'createchild') {
11✔
503
            this.checkComponentName(firstArgToken);
1✔
504
        } else if (callName === 'callfunc' && !util.isGenericNodeType(callerType)) {
10✔
505
            const funcType = util.getCallFuncType(call, firstArgToken, { flags: SymbolTypeFlag.runtime, ignoreCall: true });
7✔
506
            if (!funcType?.isResolvable()) {
7!
507
                const functionName = firstArgToken.text.replace(/"/g, '');
3✔
508
                const functionFullname = `${callerType.toString()}@.${functionName}`;
3✔
509
                this.addMultiScopeDiagnostic({
3✔
510
                    ...DiagnosticMessages.cannotFindCallFuncFunction(functionName, functionFullname, callerType.toString()),
511
                    location: firstArgToken?.location
9!
512
                });
513
            } else {
514
                this.validateFunctionCall(file, call, funcType, firstArgToken.location, call.args, 1);
4✔
515
            }
516
        }
517
    }
518

519

520
    private validateCallExpression(file: BrsFile, expression: CallExpression) {
521
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: {} };
1,074✔
522
        let funcType = this.getNodeTypeWrapper(file, expression?.callee, getTypeOptions);
1,074!
523
        if (funcType?.isResolvable() && isClassType(funcType)) {
1,074✔
524
            // We're calling a class - get the constructor
525
            funcType = funcType.getMemberType('new', getTypeOptions);
131✔
526
        }
527
        const callErrorLocation = expression?.callee?.location;
1,074!
528
        return this.validateFunctionCall(file, expression.callee, funcType, callErrorLocation, expression.args);
1,074✔
529

530
    }
531

532
    private validateCallFuncExpression(file: BrsFile, expression: CallfuncExpression) {
533
        const callerType = expression.callee?.getType({ flags: SymbolTypeFlag.runtime });
70!
534
        if (isDynamicType(callerType)) {
70✔
535
            return;
22✔
536
        }
537
        const methodToken = expression.tokens.methodName;
48✔
538
        const methodName = methodToken?.text ?? '';
48✔
539
        const functionFullname = `${callerType.toString()}@.${methodName}`;
48✔
540
        const callErrorLocation = expression.location;
48✔
541
        if (util.isGenericNodeType(callerType) || isObjectType(callerType) || isDynamicType(callerType)) {
48✔
542
            // ignore "general" node
543
            return;
7✔
544
        }
545

546
        const funcType = util.getCallFuncType(expression, methodToken, { flags: SymbolTypeFlag.runtime, ignoreCall: true });
41✔
547
        if (!funcType?.isResolvable()) {
38✔
548
            this.addMultiScopeDiagnostic({
10✔
549
                ...DiagnosticMessages.cannotFindCallFuncFunction(methodName, functionFullname, callerType.toString()),
550
                location: callErrorLocation
551
            });
552
        }
553

554
        return this.validateFunctionCall(file, expression, funcType, callErrorLocation, expression.args);
38✔
555
    }
556

557
    /**
558
     * Detect calls to functions with the incorrect number of parameters, or wrong types of arguments
559
     */
560
    private validateFunctionCall(file: BrsFile, callee: Expression, funcType: BscType, callErrorLocation: Location, args: Expression[], argOffset = 0) {
1,112✔
561
        if (!funcType?.isResolvable() || !isCallableType(funcType)) {
1,116✔
562
            const funcName = util.getAllDottedGetPartsAsString(callee, ParseMode.BrighterScript, isCallfuncExpression(callee) ? '@.' : '.');
168✔
563
            if (isUnionType(funcType)) {
168✔
564
                if (!util.isUnionOfFunctions(funcType) && !isCallfuncExpression(callee)) {
8!
565
                    // union of func and non func. not callable
UNCOV
566
                    this.addMultiScopeDiagnostic({
×
567
                        ...DiagnosticMessages.notCallable(funcName),
568
                        location: callErrorLocation
569
                    });
UNCOV
570
                    return;
×
571
                }
572
                const callablesInUnion = funcType.types.filter(isCallableType);
8✔
573
                const funcsInUnion = callablesInUnion.filter(isTypedFunctionType);
8✔
574
                if (funcsInUnion.length < callablesInUnion.length) {
8!
575
                    // potentially a non-typed func in union
576
                    // cannot validate
577
                    return;
×
578
                }
579
                // check all funcs to see if they work
580
                for (let i = 1; i < funcsInUnion.length; i++) {
8✔
581
                    const compatibilityData: TypeCompatibilityData = {};
7✔
582
                    if (!funcsInUnion[0].isTypeCompatible(funcsInUnion[i], compatibilityData)) {
7✔
583
                        if (!compatibilityData.returnTypeMismatch) {
4✔
584
                            // param differences!
585
                            this.addMultiScopeDiagnostic({
2✔
586
                                ...DiagnosticMessages.incompatibleSymbolDefinition(
587
                                    funcName,
588
                                    { isUnion: true, data: compatibilityData }),
589
                                location: callErrorLocation
590
                            });
591
                            return;
2✔
592
                        }
593
                    }
594
                }
595
                // The only thing different was return type
596
                funcType = util.getFunctionTypeFromUnion(funcType);
6✔
597

598
            }
599
            if (funcType && !isCallableType(funcType) && !isReferenceType(funcType)) {
166✔
600
                const globalFuncWithVarName = globalCallableMap.get(funcName.toLowerCase());
7✔
601
                if (globalFuncWithVarName) {
7✔
602
                    funcType = globalFuncWithVarName.type;
1✔
603
                } else {
604
                    this.addMultiScopeDiagnostic({
6✔
605
                        ...DiagnosticMessages.notCallable(funcName),
606
                        location: callErrorLocation
607
                    });
608
                    return;
6✔
609
                }
610

611
            }
612
        }
613

614
        if (!isTypedFunctionType(funcType)) {
1,108✔
615
            // non typed function. nothing to check
616
            return;
293✔
617
        }
618

619
        //get min/max parameter count for callable
620
        let minParams = 0;
815✔
621
        let maxParams = 0;
815✔
622
        for (let param of funcType.params) {
815✔
623
            maxParams++;
1,122✔
624
            //optional parameters must come last, so we can assume that minParams won't increase once we hit
625
            //the first isOptional
626
            if (param.isOptional !== true) {
1,122✔
627
                minParams++;
616✔
628
            }
629
        }
630
        if (funcType.isVariadic) {
815✔
631
            // function accepts variable number of arguments
632
            maxParams = CallExpression.MaximumArguments;
12✔
633
        }
634
        const argsForCall = argOffset < 1 ? args : args.slice(argOffset);
815✔
635

636
        let expCallArgCount = argsForCall.length;
815✔
637
        if (expCallArgCount > maxParams || expCallArgCount < minParams) {
815✔
638
            let minMaxParamsText = minParams === maxParams ? maxParams + argOffset : `${minParams + argOffset}-${maxParams + argOffset}`;
35✔
639
            this.addMultiScopeDiagnostic({
35✔
640
                ...DiagnosticMessages.mismatchArgumentCount(minMaxParamsText, expCallArgCount + argOffset),
641
                location: callErrorLocation
642
            });
643
        }
644
        let paramIndex = 0;
815✔
645
        for (let arg of argsForCall) {
815✔
646
            const data = {} as ExtraSymbolData;
708✔
647
            let argType = this.getNodeTypeWrapper(file, arg, { flags: SymbolTypeFlag.runtime, data: data });
708✔
648

649
            const paramType = funcType.params[paramIndex]?.type;
708✔
650
            if (!paramType) {
708✔
651
                // unable to find a paramType -- maybe there are more args than params
652
                break;
22✔
653
            }
654

655
            if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) {
686✔
656
                argType = data.definingNode.getConstructorType();
2✔
657
            }
658

659
            const compatibilityData: TypeCompatibilityData = {};
686✔
660
            const isAllowedArgConversion = this.checkAllowedArgConversions(paramType, argType);
686✔
661
            if (!isAllowedArgConversion && !paramType?.isTypeCompatible(argType, compatibilityData)) {
686!
662
                this.addMultiScopeDiagnostic({
51✔
663
                    ...DiagnosticMessages.argumentTypeMismatch(argType?.toString() ?? 'unknown', paramType?.toString() ?? 'unknown', compatibilityData),
612!
664
                    location: arg.location
665
                });
666
            }
667
            paramIndex++;
686✔
668
        }
669
    }
670

671
    private checkAllowedArgConversions(paramType: BscType, argType: BscType): boolean {
672
        if (isNumberTypeLike(argType) && isBooleanTypeLike(paramType)) {
686✔
673
            return true;
8✔
674
        }
675
        return false;
678✔
676
    }
677

678

679
    /**
680
     * Detect return statements with incompatible types vs. declared return type
681
     */
682
    private validateReturnStatement(file: BrsFile, returnStmt: ReturnStatement) {
683
        const data: ExtraSymbolData = {};
458✔
684
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: data };
458✔
685
        let funcType = returnStmt.findAncestor(isFunctionExpression)?.getType({ flags: SymbolTypeFlag.typetime });
458✔
686
        if (isTypedFunctionType(funcType)) {
458✔
687
            let actualReturnType = returnStmt?.value
457!
688
                ? this.getNodeTypeWrapper(file, returnStmt?.value, getTypeOptions)
1,771!
689
                : VoidType.instance;
690
            const compatibilityData: TypeCompatibilityData = {};
457✔
691

692
            // `return` statement by itself in non-built-in function will actually result in `invalid`
693
            const valueReturnType = isVoidType(actualReturnType) ? InvalidType.instance : actualReturnType;
457✔
694

695
            if (funcType.returnType.isResolvable()) {
457✔
696
                if (!returnStmt?.value && isVoidType(funcType.returnType)) {
453!
697
                    // allow empty return when function is return `as void`
698
                    // eslint-disable-next-line no-useless-return
699
                    return;
8✔
700
                } else if (!funcType.returnType.isTypeCompatible(valueReturnType, compatibilityData)) {
445✔
701
                    this.addMultiScopeDiagnostic({
31✔
702
                        ...DiagnosticMessages.returnTypeMismatch(actualReturnType.toString(), funcType.returnType.toString(), compatibilityData),
703
                        location: returnStmt.value?.location ?? returnStmt.location
186✔
704
                    });
705
                }
706
            }
707
        }
708
    }
709

710
    /**
711
     * Detect assigned type different from expected member type
712
     */
713
    private validateDottedSetStatement(file: BrsFile, dottedSetStmt: DottedSetStatement) {
714
        const typeChainExpectedLHS = [] as TypeChainEntry[];
103✔
715
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
103✔
716

717
        const expectedLHSType = this.getNodeTypeWrapper(file, dottedSetStmt, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
103✔
718
        const actualRHSType = this.getNodeTypeWrapper(file, dottedSetStmt?.value, getTypeOpts);
103!
719
        const compatibilityData: TypeCompatibilityData = {};
103✔
720
        const typeChainScan = util.processTypeChain(typeChainExpectedLHS);
103✔
721
        // check if anything in typeChain is an AA - if so, just allow it
722
        if (typeChainExpectedLHS.find(typeChainItem => isAssociativeArrayType(typeChainItem.type))) {
181✔
723
            // something in the chain is an AA
724
            // treat members as dynamic - they could have been set without the type system's knowledge
725
            return;
39✔
726
        }
727
        if (!expectedLHSType || !expectedLHSType?.isResolvable()) {
64!
728
            this.addMultiScopeDiagnostic({
5✔
729
                ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
730
                location: typeChainScan?.location
15!
731
            });
732
            return;
5✔
733
        }
734

735
        let accessibilityIsOk = this.checkMemberAccessibility(file, dottedSetStmt, typeChainExpectedLHS);
59✔
736

737
        //Most Component fields can be set with strings
738
        //TODO: be more precise about which fields can actually accept strings
739
        //TODO: if RHS is a string literal, we can do more validation to make sure it's the correct type
740
        if (isComponentType(dottedSetStmt.obj?.getType({ flags: SymbolTypeFlag.runtime }))) {
59!
741
            if (isStringTypeLike(actualRHSType)) {
21✔
742
                return;
6✔
743
            }
744
        }
745

746
        if (accessibilityIsOk && !expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
53!
747
            this.addMultiScopeDiagnostic({
13✔
748
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType?.toString() ?? 'unknown', expectedLHSType?.toString() ?? 'unknown', compatibilityData),
156!
749
                location: dottedSetStmt.location
750
            });
751
        }
752
    }
753

754
    /**
755
     * Detect when declared type does not match rhs type
756
     */
757
    private validateAssignmentStatement(file: BrsFile, assignStmt: AssignmentStatement) {
758
        if (!assignStmt?.typeExpression) {
827!
759
            // nothing to check
760
            return;
812✔
761
        }
762

763
        const typeChainExpectedLHS = [];
15✔
764
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
15✔
765
        const expectedLHSType = this.getNodeTypeWrapper(file, assignStmt.typeExpression, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
15✔
766
        const actualRHSType = this.getNodeTypeWrapper(file, assignStmt.value, getTypeOpts);
15✔
767
        const compatibilityData: TypeCompatibilityData = {};
15✔
768
        if (!expectedLHSType || !expectedLHSType.isResolvable()) {
15✔
769
            // LHS is not resolvable... handled elsewhere
770
        } else if (!expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
14!
771
            this.addMultiScopeDiagnostic({
2✔
772
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
773
                location: assignStmt.location
774
            });
775
        }
776
    }
777

778
    /**
779
     * Detect invalid use of a binary operator
780
     */
781
    private validateBinaryExpression(file: BrsFile, binaryExpr: BinaryExpression | AugmentedAssignmentStatement) {
782
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
455✔
783

784
        if (util.isInTypeExpression(binaryExpr)) {
455✔
785
            return;
78✔
786
        }
787

788
        let leftType = isBinaryExpression(binaryExpr)
377✔
789
            ? this.getNodeTypeWrapper(file, binaryExpr.left, getTypeOpts)
377✔
790
            : this.getNodeTypeWrapper(file, binaryExpr.item, getTypeOpts);
791
        let rightType = isBinaryExpression(binaryExpr)
377✔
792
            ? this.getNodeTypeWrapper(file, binaryExpr.right, getTypeOpts)
377✔
793
            : this.getNodeTypeWrapper(file, binaryExpr.value, getTypeOpts);
794

795
        if (!leftType || !rightType || !leftType.isResolvable() || !rightType.isResolvable()) {
377✔
796
            // Can not find the type. error handled elsewhere
797
            return;
13✔
798
        }
799

800
        let leftTypeToTest = leftType;
364✔
801
        let rightTypeToTest = rightType;
364✔
802

803
        if (isEnumMemberType(leftType) || isEnumType(leftType)) {
364✔
804
            leftTypeToTest = leftType.underlyingType;
11✔
805
        }
806
        if (isEnumMemberType(rightType) || isEnumType(rightType)) {
364✔
807
            rightTypeToTest = rightType.underlyingType;
10✔
808
        }
809

810
        if (isUnionType(leftType) || isUnionType(rightType)) {
364✔
811
            // TODO: it is possible to validate based on innerTypes, but more complicated
812
            // Because you need to verify each combination of types
813
            return;
7✔
814
        }
815
        const opResult = util.binaryOperatorResultType(leftTypeToTest, binaryExpr.tokens.operator, rightTypeToTest);
357✔
816

817
        if (!opResult) {
357✔
818
            // if the result was dynamic or void, that means there wasn't a valid operation
819
            this.addMultiScopeDiagnostic({
10✔
820
                ...DiagnosticMessages.operatorTypeMismatch(binaryExpr.tokens.operator.text, leftType.toString(), rightType.toString()),
821
                location: binaryExpr.location
822
            });
823
        }
824
    }
825

826
    /**
827
     * Detect invalid use of a Unary operator
828
     */
829
    private validateUnaryExpression(file: BrsFile, unaryExpr: UnaryExpression) {
830
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
36✔
831

832
        let rightType = this.getNodeTypeWrapper(file, unaryExpr.right, getTypeOpts);
36✔
833

834
        if (!rightType.isResolvable()) {
36!
835
            // Can not find the type. error handled elsewhere
UNCOV
836
            return;
×
837
        }
838
        let rightTypeToTest = rightType;
36✔
839
        if (isEnumMemberType(rightType)) {
36!
UNCOV
840
            rightTypeToTest = rightType.underlyingType;
×
841
        }
842

843
        if (isUnionType(rightTypeToTest)) {
36✔
844
            // TODO: it is possible to validate based on innerTypes, but more complicated
845
            // Because you need to verify each combination of types
846

847
        } else if (isDynamicType(rightTypeToTest) || isObjectType(rightTypeToTest)) {
35✔
848
            // operand is basically "any" type... ignore;
849

850
        } else if (isPrimitiveType(rightType)) {
31!
851
            const opResult = util.unaryOperatorResultType(unaryExpr.tokens.operator, rightTypeToTest);
31✔
852
            if (!opResult) {
31✔
853
                this.addMultiScopeDiagnostic({
1✔
854
                    ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
855
                    location: unaryExpr.location
856
                });
857
            }
858
        } else {
859
            // rhs is not a primitive, so no binary operator is allowed
UNCOV
860
            this.addMultiScopeDiagnostic({
×
861
                ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
862
                location: unaryExpr.location
863
            });
864
        }
865
    }
866

867
    private validateIncrementStatement(file: BrsFile, incStmt: IncrementStatement) {
868
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
11✔
869

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

872
        if (!rightType.isResolvable()) {
11!
873
            // Can not find the type. error handled elsewhere
UNCOV
874
            return;
×
875
        }
876

877
        if (isUnionType(rightType)) {
11!
878
            // TODO: it is possible to validate based on innerTypes, but more complicated
879
            // because you need to verify each combination of types
880
        } else if (isDynamicType(rightType) || isObjectType(rightType)) {
11✔
881
            // operand is basically "any" type... ignore
882
        } else if (isNumberTypeLike(rightType)) {
10✔
883
            // operand is a number.. this is ok
884
        } else {
885
            // rhs is not a number, so no increment operator is not allowed
886
            this.addMultiScopeDiagnostic({
1✔
887
                ...DiagnosticMessages.operatorTypeMismatch(incStmt.tokens.operator.text, rightType.toString()),
888
                location: incStmt.location
889
            });
890
        }
891
    }
892

893

894
    validateVariableAndDottedGetExpressions(file: BrsFile, expression: VariableExpression | DottedGetExpression) {
895
        if (isDottedGetExpression(expression.parent)) {
6,528✔
896
            // We validate dottedGetExpressions at the top-most level
897
            return;
1,588✔
898
        }
899
        if (isVariableExpression(expression)) {
4,940✔
900
            if (isAssignmentStatement(expression.parent) && expression.parent.tokens.name === expression.tokens.name) {
3,698!
901
                // Don't validate LHS of assignments
UNCOV
902
                return;
×
903
            } else if (isNamespaceStatement(expression.parent)) {
3,698✔
904
                return;
3✔
905
            }
906
        }
907

908
        let symbolType = SymbolTypeFlag.runtime;
4,937✔
909
        let oppositeSymbolType = SymbolTypeFlag.typetime;
4,937✔
910
        const isUsedAsType = util.isInTypeExpression(expression);
4,937✔
911
        if (isUsedAsType) {
4,937✔
912
            // This is used in a TypeExpression - only look up types from SymbolTable
913
            symbolType = SymbolTypeFlag.typetime;
1,695✔
914
            oppositeSymbolType = SymbolTypeFlag.runtime;
1,695✔
915
        }
916

917
        // Do a complete type check on all DottedGet and Variable expressions
918
        // this will create a diagnostic if an invalid member is accessed
919
        const typeChain: TypeChainEntry[] = [];
4,937✔
920
        const typeData = {} as ExtraSymbolData;
4,937✔
921
        let exprType = this.getNodeTypeWrapper(file, expression, {
4,937✔
922
            flags: symbolType,
923
            typeChain: typeChain,
924
            data: typeData
925
        });
926

927
        const hasValidDeclaration = this.hasValidDeclaration(expression, exprType, typeData?.definingNode);
4,937!
928

929
        //include a hint diagnostic if this type is marked as deprecated
930
        if (typeData.flags & SymbolTypeFlag.deprecated) { // eslint-disable-line no-bitwise
4,937✔
931
            this.addMultiScopeDiagnostic({
2✔
932
                ...DiagnosticMessages.itemIsDeprecated(),
933
                location: expression.tokens.name.location,
934
                tags: [DiagnosticTag.Deprecated]
935
            });
936
        }
937

938
        if (!this.isTypeKnown(exprType) && !hasValidDeclaration) {
4,937✔
939
            if (this.getNodeTypeWrapper(file, expression, { flags: oppositeSymbolType, isExistenceTest: true })?.isResolvable()) {
271!
940
                const oppoSiteTypeChain = [];
5✔
941
                const invalidlyUsedResolvedType = this.getNodeTypeWrapper(file, expression, { flags: oppositeSymbolType, typeChain: oppoSiteTypeChain, isExistenceTest: true });
5✔
942
                const typeChainScan = util.processTypeChain(oppoSiteTypeChain);
5✔
943
                if (isUsedAsType) {
5✔
944
                    this.addMultiScopeDiagnostic({
2✔
945
                        ...DiagnosticMessages.itemCannotBeUsedAsType(typeChainScan.fullChainName),
946
                        location: expression.location
947
                    });
948
                } else if (invalidlyUsedResolvedType && !isReferenceType(invalidlyUsedResolvedType)) {
3✔
949
                    if (!isAliasStatement(expression.parent)) {
1!
950
                        // alias rhs CAN be a type!
UNCOV
951
                        this.addMultiScopeDiagnostic({
×
952
                            ...DiagnosticMessages.itemCannotBeUsedAsVariable(invalidlyUsedResolvedType.toString()),
953
                            location: expression.location
954
                        });
955
                    }
956
                } else {
957
                    const typeChainScan = util.processTypeChain(typeChain);
2✔
958
                    //if this is a function call, provide a different diagnostic code
959
                    if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
2✔
960
                        this.addMultiScopeDiagnostic({
1✔
961
                            ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
962
                            location: typeChainScan?.location
3!
963
                        });
964
                    } else {
965
                        this.addMultiScopeDiagnostic({
1✔
966
                            ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
967
                            location: typeChainScan?.location
3!
968
                        });
969
                    }
970
                }
971

972
            } else if (!(typeData?.isFromDocComment)) {
266!
973
                // only show "cannot find... " errors if the type is not defined from a doc comment
974
                const typeChainScan = util.processTypeChain(typeChain);
264✔
975
                const circularReferenceInfo = this.getCircularReference(exprType);
264✔
976
                if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
264✔
977
                    this.addMultiScopeDiagnostic({
27✔
978
                        ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
979
                        location: typeChainScan?.location
81!
980
                    });
981
                } else if (circularReferenceInfo?.isCircularReference) {
237!
982
                    let diagnosticDetail = util.getCircularReferenceDiagnosticDetail(circularReferenceInfo, typeChainScan.fullNameOfItem);
9✔
983
                    this.addMultiScopeDiagnostic({
9✔
984
                        ...DiagnosticMessages.circularReferenceDetected(diagnosticDetail),
985
                        location: typeChainScan?.location
27!
986
                    });
987
                } else {
988
                    this.addMultiScopeDiagnostic({
228✔
989
                        ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
990
                        location: typeChainScan?.location
684!
991
                    });
992
                }
993

994
            }
995
        }
996
        if (isUsedAsType) {
4,937✔
997
            return;
1,695✔
998
        }
999

1000
        const containingNamespace = expression.findAncestor<NamespaceStatement>(isNamespaceStatement);
3,242✔
1001
        const containingNamespaceName = containingNamespace?.getName(ParseMode.BrighterScript);
3,242✔
1002

1003
        if (!(isCallExpression(expression.parent) && isNewExpression(expression.parent?.parent))) {
3,242!
1004
            const classUsedAsVarEntry = this.checkTypeChainForClassUsedAsVar(typeChain, containingNamespaceName);
3,121✔
1005
            const isClassInNamespace = containingNamespace?.getSymbolTable().hasSymbol(typeChain[0].name, SymbolTypeFlag.runtime);
3,121✔
1006
            if (classUsedAsVarEntry && !isClassInNamespace) {
3,121!
1007

UNCOV
1008
                this.addMultiScopeDiagnostic({
×
1009
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable(classUsedAsVarEntry.toString()),
1010
                    location: expression.location
1011
                });
UNCOV
1012
                return;
×
1013
            }
1014
        }
1015

1016
        const lastTypeInfo = typeChain[typeChain.length - 1];
3,242✔
1017
        const parentTypeInfo = typeChain[typeChain.length - 2];
3,242✔
1018

1019
        this.checkMemberAccessibility(file, expression, typeChain);
3,242✔
1020

1021
        if (isNamespaceType(exprType) && !isAliasStatement(expression.parent)) {
3,242✔
1022
            this.addMultiScopeDiagnostic({
24✔
1023
                ...DiagnosticMessages.itemCannotBeUsedAsVariable('namespace'),
1024
                location: expression.location
1025
            });
1026
        } else if (isEnumType(exprType) && !isAliasStatement(expression.parent)) {
3,218✔
1027
            const enumStatement = this.event.scope.getEnum(util.getAllDottedGetPartsAsString(expression));
27✔
1028
            if (enumStatement) {
27✔
1029
                // there's an enum with this name
1030
                this.addMultiScopeDiagnostic({
4✔
1031
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable('enum'),
1032
                    location: expression.location
1033
                });
1034
            }
1035
        } else if (isDynamicType(exprType) && isEnumType(parentTypeInfo?.type) && isDottedGetExpression(expression)) {
3,191✔
1036
            const enumFileLink = this.event.scope.getEnumFileLink(util.getAllDottedGetPartsAsString(expression.obj));
12✔
1037
            const typeChainScanForItem = util.processTypeChain(typeChain);
12✔
1038
            const typeChainScanForParent = util.processTypeChain(typeChain.slice(0, -1));
12✔
1039
            if (enumFileLink) {
12✔
1040
                this.addMultiScopeDiagnostic({
8✔
1041
                    ...DiagnosticMessages.cannotFindName(lastTypeInfo?.name, typeChainScanForItem.fullChainName, typeChainScanForParent.fullNameOfItem, 'enum'),
24!
1042
                    location: lastTypeInfo?.location,
24!
1043
                    relatedInformation: [{
1044
                        message: 'Enum declared here',
1045
                        location: util.createLocationFromRange(
1046
                            util.pathToUri(enumFileLink?.file.srcPath),
24!
1047
                            enumFileLink?.item?.tokens.name.location?.range
72!
1048
                        )
1049
                    }]
1050
                });
1051
            }
1052
        }
1053
    }
1054

1055
    private checkTypeChainForClassUsedAsVar(typeChain: TypeChainEntry[], containingNamespaceName: string) {
1056
        const ignoreKinds = [AstNodeKind.TypecastExpression, AstNodeKind.NewExpression];
3,121✔
1057
        let lowerNameSoFar = '';
3,121✔
1058
        let classUsedAsVar;
1059
        let isFirst = true;
3,121✔
1060
        for (let i = 0; i < typeChain.length - 1; i++) { // do not look at final entry - we CAN use the constructor as a variable
3,121✔
1061
            const tce = typeChain[i];
1,475✔
1062
            lowerNameSoFar += `${lowerNameSoFar ? '.' : ''}${tce.name.toLowerCase()}`;
1,475✔
1063
            if (!isNamespaceType(tce.type)) {
1,475✔
1064
                if (isFirst && containingNamespaceName) {
765✔
1065
                    lowerNameSoFar = `${containingNamespaceName.toLowerCase()}.${lowerNameSoFar}`;
78✔
1066
                }
1067
                if (!tce.astNode || ignoreKinds.includes(tce.astNode.kind)) {
765✔
1068
                    break;
15✔
1069
                } else if (isClassType(tce.type) && lowerNameSoFar.toLowerCase() === tce.type.name.toLowerCase()) {
750✔
1070
                    classUsedAsVar = tce.type;
1✔
1071
                }
1072
                break;
750✔
1073
            }
1074
            isFirst = false;
710✔
1075
        }
1076

1077
        return classUsedAsVar;
3,121✔
1078
    }
1079

1080
    /**
1081
     * Adds diagnostics for accibility mismatches
1082
     *
1083
     * @param file file
1084
     * @param expression containing expression
1085
     * @param typeChain type chain to check
1086
     * @returns true if member accesiibility is okay
1087
     */
1088
    private checkMemberAccessibility(file: BscFile, expression: Expression, typeChain: TypeChainEntry[]) {
1089
        for (let i = 0; i < typeChain.length - 1; i++) {
3,301✔
1090
            const parentChainItem = typeChain[i];
1,669✔
1091
            const childChainItem = typeChain[i + 1];
1,669✔
1092
            if (isClassType(parentChainItem.type)) {
1,669✔
1093
                const containingClassStmt = expression.findAncestor<ClassStatement>(isClassStatement);
159✔
1094
                const classStmtThatDefinesChildMember = childChainItem.data?.definingNode?.findAncestor<ClassStatement>(isClassStatement);
159!
1095
                if (classStmtThatDefinesChildMember) {
159✔
1096
                    const definingClassName = classStmtThatDefinesChildMember.getName(ParseMode.BrighterScript);
157✔
1097
                    const inMatchingClassStmt = containingClassStmt?.getName(ParseMode.BrighterScript).toLowerCase() === parentChainItem.type.name.toLowerCase();
157✔
1098
                    // eslint-disable-next-line no-bitwise
1099
                    if (childChainItem.data.flags & SymbolTypeFlag.private) {
157✔
1100
                        if (!inMatchingClassStmt || childChainItem.data.memberOfAncestor) {
16✔
1101
                            this.addMultiScopeDiagnostic({
4✔
1102
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
1103
                                location: expression.location
1104
                            });
1105
                            // there's an error... don't worry about the rest of the chain
1106
                            return false;
4✔
1107
                        }
1108
                    }
1109

1110
                    // eslint-disable-next-line no-bitwise
1111
                    if (childChainItem.data.flags & SymbolTypeFlag.protected) {
153✔
1112
                        const containingClassName = containingClassStmt?.getName(ParseMode.BrighterScript);
13✔
1113
                        const containingNamespaceName = expression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
13✔
1114
                        const ancestorClasses = this.event.scope.getClassHierarchy(containingClassName, containingNamespaceName).map(link => link.item);
13✔
1115
                        const isSubClassOfDefiningClass = ancestorClasses.includes(classStmtThatDefinesChildMember);
13✔
1116

1117
                        if (!isSubClassOfDefiningClass) {
13✔
1118
                            this.addMultiScopeDiagnostic({
5✔
1119
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
1120
                                location: expression.location
1121
                            });
1122
                            // there's an error... don't worry about the rest of the chain
1123
                            return false;
5✔
1124
                        }
1125
                    }
1126
                }
1127

1128
            }
1129
        }
1130
        return true;
3,292✔
1131
    }
1132

1133
    /**
1134
     * Find all "new" statements in the program,
1135
     * and make sure we can find a class with that name
1136
     */
1137
    private validateNewExpression(file: BrsFile, newExpression: NewExpression) {
1138
        const newExprType = this.getNodeTypeWrapper(file, newExpression, { flags: SymbolTypeFlag.typetime });
121✔
1139
        if (isClassType(newExprType)) {
121✔
1140
            return;
113✔
1141
        }
1142

1143
        let potentialClassName = newExpression.className.getName(ParseMode.BrighterScript);
8✔
1144
        const namespaceName = newExpression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
8!
1145
        let newableClass = this.event.scope.getClass(potentialClassName, namespaceName);
8✔
1146

1147
        if (!newableClass) {
8!
1148
            //try and find functions with this name.
1149
            let fullName = util.getFullyQualifiedClassName(potentialClassName, namespaceName);
8✔
1150

1151
            this.addMultiScopeDiagnostic({
8✔
1152
                ...DiagnosticMessages.expressionIsNotConstructable(fullName),
1153
                location: newExpression.className.location
1154
            });
1155

1156
        }
1157
    }
1158

1159
    private validateFunctionExpressionForReturn(func: FunctionExpression) {
1160
        const returnType = func?.returnTypeExpression?.getType({ flags: SymbolTypeFlag.typetime });
2,396!
1161

1162
        if (!returnType || !returnType.isResolvable() || isVoidType(returnType) || isDynamicType(returnType)) {
2,396✔
1163
            return;
2,103✔
1164
        }
1165
        const returns = func.body?.findChild<ReturnStatement>(isReturnStatement, { walkMode: WalkMode.visitAll });
293!
1166
        if (!returns && isStringTypeLike(returnType)) {
293✔
1167
            this.addMultiScopeDiagnostic({
5✔
1168
                ...DiagnosticMessages.returnTypeCoercionMismatch(returnType.toString()),
1169
                location: func.location
1170
            });
1171
        }
1172
    }
1173

1174
    /**
1175
     * Create diagnostics for any duplicate function declarations
1176
     */
1177
    private flagDuplicateFunctionDeclarations() {
1178
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration });
2,084✔
1179

1180
        //for each list of callables with the same name
1181
        for (let [lowerName, callableContainers] of this.event.scope.getCallableContainerMap()) {
2,084✔
1182

1183
            let globalCallables = [] as CallableContainer[];
156,491✔
1184
            let nonGlobalCallables = [] as CallableContainer[];
156,491✔
1185
            let ownCallables = [] as CallableContainer[];
156,491✔
1186
            let ancestorNonGlobalCallables = [] as CallableContainer[];
156,491✔
1187

1188

1189
            for (let container of callableContainers) {
156,491✔
1190
                if (container.scope === this.event.program.globalScope) {
162,777✔
1191
                    globalCallables.push(container);
160,468✔
1192
                } else {
1193
                    nonGlobalCallables.push(container);
2,309✔
1194
                    if (container.scope === this.event.scope) {
2,309✔
1195
                        ownCallables.push(container);
2,279✔
1196
                    } else {
1197
                        ancestorNonGlobalCallables.push(container);
30✔
1198
                    }
1199
                }
1200
            }
1201

1202
            //add info diagnostics about child shadowing parent functions
1203
            if (ownCallables.length > 0 && ancestorNonGlobalCallables.length > 0) {
156,491✔
1204
                for (let container of ownCallables) {
24✔
1205
                    //skip the init function (because every component will have one of those){
1206
                    if (lowerName !== 'init') {
24✔
1207
                        let shadowedCallable = ancestorNonGlobalCallables[ancestorNonGlobalCallables.length - 1];
23✔
1208
                        if (!!shadowedCallable && shadowedCallable.callable.file === container.callable.file) {
23✔
1209
                            //same file: skip redundant imports
1210
                            continue;
20✔
1211
                        }
1212
                        this.addMultiScopeDiagnostic({
3✔
1213
                            ...DiagnosticMessages.overridesAncestorFunction(
1214
                                container.callable.name,
1215
                                container.scope.name,
1216
                                shadowedCallable.callable.file.destPath,
1217
                                //grab the last item in the list, which should be the closest ancestor's version
1218
                                shadowedCallable.scope.name
1219
                            ),
1220
                            location: util.createLocationFromFileRange(container.callable.file, container.callable.nameRange)
1221
                        }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
1222
                    }
1223
                }
1224
            }
1225

1226
            //add error diagnostics about duplicate functions in the same scope
1227
            if (ownCallables.length > 1) {
156,491✔
1228

1229
                for (let callableContainer of ownCallables) {
5✔
1230
                    let callable = callableContainer.callable;
10✔
1231
                    const related = [];
10✔
1232
                    for (const ownCallable of ownCallables) {
10✔
1233
                        const thatNameRange = ownCallable.callable.nameRange;
20✔
1234
                        if (ownCallable.callable.nameRange !== callable.nameRange) {
20✔
1235
                            related.push({
10✔
1236
                                message: `Function declared here`,
1237
                                location: util.createLocationFromRange(
1238
                                    util.pathToUri(ownCallable.callable.file?.srcPath),
30!
1239
                                    thatNameRange
1240
                                )
1241
                            });
1242
                        }
1243
                    }
1244

1245
                    this.addMultiScopeDiagnostic({
10✔
1246
                        ...DiagnosticMessages.duplicateFunctionImplementation(callable.name),
1247
                        location: util.createLocationFromFileRange(callable.file, callable.nameRange),
1248
                        relatedInformation: related
1249
                    }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
1250
                }
1251
            }
1252
        }
1253
    }
1254

1255
    /**
1256
     * Verify that all of the scripts imported by each file in this scope actually exist, and have the correct case
1257
     */
1258
    private validateScriptImportPaths() {
1259
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Imports });
2,084✔
1260

1261
        let scriptImports = this.event.scope.getOwnScriptImports();
2,084✔
1262
        //verify every script import
1263
        for (let scriptImport of scriptImports) {
2,084✔
1264
            let referencedFile = this.event.scope.getFileByRelativePath(scriptImport.destPath);
613✔
1265
            //if we can't find the file
1266
            if (!referencedFile) {
613✔
1267
                //skip the default bslib file, it will exist at transpile time but should not show up in the program during validation cycle
1268
                if (scriptImport.destPath === this.event.program.bslibPkgPath) {
17✔
1269
                    continue;
2✔
1270
                }
1271
                let dInfo: DiagnosticInfo;
1272
                if (scriptImport.text.trim().length === 0) {
15✔
1273
                    dInfo = DiagnosticMessages.scriptSrcCannotBeEmpty();
1✔
1274
                } else {
1275
                    dInfo = DiagnosticMessages.referencedFileDoesNotExist();
14✔
1276
                }
1277

1278
                this.addMultiScopeDiagnostic({
15✔
1279
                    ...dInfo,
1280
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
1281
                }, ScopeValidatorDiagnosticTag.Imports);
1282
                //if the character casing of the script import path does not match that of the actual path
1283
            } else if (scriptImport.destPath !== referencedFile.destPath) {
596✔
1284
                this.addMultiScopeDiagnostic({
2✔
1285
                    ...DiagnosticMessages.scriptImportCaseMismatch(referencedFile.destPath),
1286
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
1287
                }, ScopeValidatorDiagnosticTag.Imports);
1288
            }
1289
        }
1290
    }
1291

1292
    /**
1293
     * Validate all classes defined in this scope
1294
     */
1295
    private validateClasses() {
1296
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Classes });
2,084✔
1297

1298
        let validator = new BsClassValidator(this.event.scope);
2,084✔
1299
        validator.validate();
2,084✔
1300
        for (const diagnostic of validator.diagnostics) {
2,084✔
1301
            this.addMultiScopeDiagnostic({
29✔
1302
                ...diagnostic
1303
            }, ScopeValidatorDiagnosticTag.Classes);
1304
        }
1305
    }
1306

1307

1308
    /**
1309
     * Find various function collisions
1310
     */
1311
    private diagnosticDetectFunctionCollisions(file: BrsFile) {
1312
        const fileUri = util.pathToUri(file.srcPath);
2,439✔
1313
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, tag: ScopeValidatorDiagnosticTag.FunctionCollisions });
2,439✔
1314
        for (let func of file.callables) {
2,439✔
1315
            const funcName = func.getName(ParseMode.BrighterScript);
2,238✔
1316
            const lowerFuncName = funcName?.toLowerCase();
2,238!
1317
            if (lowerFuncName) {
2,238!
1318

1319
                //find function declarations with the same name as a stdlib function
1320
                if (globalCallableMap.has(lowerFuncName)) {
2,238✔
1321
                    this.addMultiScopeDiagnostic({
5✔
1322
                        ...DiagnosticMessages.scopeFunctionShadowedByBuiltInFunction(),
1323
                        location: util.createLocationFromRange(fileUri, func.nameRange)
1324

1325
                    });
1326
                }
1327
            }
1328
        }
1329
    }
1330

1331
    public detectShadowedLocalVar(file: BrsFile, varDeclaration: { expr: AstNode; name: string; type: BscType; nameRange: Range }) {
1332
        const varName = varDeclaration.name;
2,193✔
1333
        const lowerVarName = varName.toLowerCase();
2,193✔
1334
        const callableContainerMap = this.event.scope.getCallableContainerMap();
2,193✔
1335
        const containingNamespace = varDeclaration.expr?.findAncestor<NamespaceStatement>(isNamespaceStatement);
2,193!
1336
        const localVarIsInNamespace = util.isVariableMemberOfNamespace(varDeclaration.name, varDeclaration.expr, containingNamespace);
2,193✔
1337

1338
        const varIsFunction = () => {
2,193✔
1339
            return isCallableType(varDeclaration.type) && !isDynamicType(varDeclaration.type);
11✔
1340
        };
1341

1342
        if (
2,193✔
1343
            //has same name as stdlib
1344
            globalCallableMap.has(lowerVarName)
1345
        ) {
1346
            //local var function with same name as stdlib function
1347
            if (varIsFunction()) {
8✔
1348
                this.addMultiScopeDiagnostic({
1✔
1349
                    ...DiagnosticMessages.localVarFunctionShadowsParentFunction('stdlib'),
1350
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange)
1351
                });
1352
            }
1353
        } else if (callableContainerMap.has(lowerVarName) && !localVarIsInNamespace) {
2,185✔
1354
            const callable = callableContainerMap.get(lowerVarName);
3✔
1355
            //is same name as a callable
1356
            if (varIsFunction()) {
3✔
1357
                this.addMultiScopeDiagnostic({
1✔
1358
                    ...DiagnosticMessages.localVarFunctionShadowsParentFunction('scope'),
1359
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1360
                    relatedInformation: [{
1361
                        message: 'Function declared here',
1362
                        location: util.createLocationFromFileRange(
1363
                            callable[0].callable.file,
1364
                            callable[0].callable.nameRange
1365
                        )
1366
                    }]
1367
                });
1368
            } else {
1369
                this.addMultiScopeDiagnostic({
2✔
1370
                    ...DiagnosticMessages.localVarShadowedByScopedFunction(),
1371
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1372
                    relatedInformation: [{
1373
                        message: 'Function declared here',
1374
                        location: util.createLocationFromRange(
1375
                            util.pathToUri(callable[0].callable.file.srcPath),
1376
                            callable[0].callable.nameRange
1377
                        )
1378
                    }]
1379
                });
1380
            }
1381
            //has the same name as an in-scope class
1382
        } else if (!localVarIsInNamespace) {
2,182✔
1383
            const classStmtLink = this.event.scope.getClassFileLink(lowerVarName);
2,181✔
1384
            if (classStmtLink) {
2,181✔
1385
                this.addMultiScopeDiagnostic({
3✔
1386
                    ...DiagnosticMessages.localVarShadowedByScopedFunction(),
1387
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1388
                    relatedInformation: [{
1389
                        message: 'Class declared here',
1390
                        location: util.createLocationFromRange(
1391
                            util.pathToUri(classStmtLink.file.srcPath),
1392
                            classStmtLink?.item.tokens.name.location?.range
18!
1393
                        )
1394
                    }]
1395
                });
1396
            }
1397
        }
1398
    }
1399

1400
    private validateForStatement(file: BrsFile, forStmt: ForStatement) {
1401
        const assignStmt = forStmt.counterDeclaration;
21✔
1402
        const assignValueType = this.getNodeTypeWrapper(file, assignStmt.value, { flags: SymbolTypeFlag.runtime, statementIndex: forStmt.statementIndex });
21✔
1403
        if (!IntegerType.instance.isTypeCompatible(assignValueType)) {
21✔
1404
            this.addMultiScopeDiagnostic({
1✔
1405
                ...DiagnosticMessages.assignmentTypeMismatch(assignValueType.toString(), 'integer'),
1406
                location: assignStmt.location
1407
            });
1408
        }
1409
        if (forStmt.increment) {
21✔
1410
            const incrementValueType = this.getNodeTypeWrapper(file, forStmt.increment, { flags: SymbolTypeFlag.runtime, statementIndex: forStmt.statementIndex });
7✔
1411
            if (!IntegerType.instance.isTypeCompatible(incrementValueType)) {
7✔
1412
                this.addMultiScopeDiagnostic({
1✔
1413
                    ...DiagnosticMessages.assignmentTypeMismatch(incrementValueType.toString(), 'integer'),
1414
                    location: forStmt.increment.location
1415
                });
1416
            }
1417
        }
1418
        const finalValueType = this.getNodeTypeWrapper(file, forStmt.finalValue, { flags: SymbolTypeFlag.runtime, statementIndex: forStmt.statementIndex });
21✔
1419
        if (!IntegerType.instance.isTypeCompatible(finalValueType)) {
21✔
1420
            this.addMultiScopeDiagnostic({
1✔
1421
                ...DiagnosticMessages.assignmentTypeMismatch(finalValueType.toString(), 'integer'),
1422
                location: forStmt.finalValue.location
1423
            });
1424
        }
1425
    }
1426

1427
    private validateForEachStatement(file: BrsFile, forEachStmt: ForEachStatement) {
1428
        const targetType = this.getNodeTypeWrapper(file, forEachStmt.target, { flags: SymbolTypeFlag.runtime, statementIndex: forEachStmt.statementIndex });
51✔
1429
        if (isDynamicType(targetType) || isObjectType(targetType)) {
51✔
1430
            // unable to determine type, skip further validation
1431
            return;
10✔
1432
        }
1433

1434
        let targetItemType: BscType;
1435

1436
        if (!isArrayType(targetType)) {
41✔
1437
            if (isIterableType(targetType)) {
12✔
1438
                // this is enumerable
1439
                targetItemType = util.getIteratorDefaultType(targetType);
10✔
1440
            } else {
1441
                // target is not an array nor enumerable
1442
                this.addMultiScopeDiagnostic({
2✔
1443
                    ...DiagnosticMessages.notIterable(targetType.toString()),
1444
                    location: forEachStmt.target.location
1445
                });
1446
                return;
2✔
1447
            }
1448
        } else {
1449
            targetItemType = targetType.defaultType;
29✔
1450
        }
1451

1452
        const loopType = forEachStmt.getLoopVariableType({ flags: SymbolTypeFlag.runtime, statementIndex: forEachStmt.statementIndex });
39✔
1453
        if (loopType?.isResolvable()) {
39!
1454

1455
            const data: TypeCompatibilityData = {};
39✔
1456
            if (!loopType.isTypeCompatible(targetItemType, data)) {
39✔
1457
                this.addMultiScopeDiagnostic({
4✔
1458
                    ...DiagnosticMessages.assignmentTypeMismatch(targetItemType.toString(), loopType.toString(), data),
1459
                    location: forEachStmt.typeExpression.location
1460
                });
1461
            }
1462
        }
1463
    }
1464

1465
    private validateXmlInterface(scope: XmlScope) {
1466
        if (!scope.xmlFile.parser.ast?.componentElement?.interfaceElement) {
544!
1467
            return;
449✔
1468
        }
1469
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLInterface });
95!
1470

1471
        const iface = scope.xmlFile.parser.ast.componentElement.interfaceElement;
95✔
1472
        const callableContainerMap = scope.getCallableContainerMap();
95✔
1473
        //validate functions
1474
        for (const func of iface.functions) {
95✔
1475
            const name = func.name;
85✔
1476
            if (!name) {
85✔
1477
                this.addDiagnostic({
3✔
1478
                    ...DiagnosticMessages.xmlTagMissingAttribute(func.tokens.startTagName.text, 'name'),
1479
                    location: func.tokens.startTagName.location
1480
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1481
            } else if (!callableContainerMap.has(name.toLowerCase())) {
82✔
1482
                this.addDiagnostic({
4✔
1483
                    ...DiagnosticMessages.xmlFunctionNotFound(name),
1484
                    location: func.getAttribute('name')?.tokens.value.location
12!
1485
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1486
            }
1487
        }
1488
        //validate fields
1489
        for (const field of iface.fields) {
95✔
1490
            const { id, type, onChange } = field;
46✔
1491
            if (!id) {
46✔
1492
                this.addDiagnostic({
3✔
1493
                    ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'id'),
1494
                    location: field.tokens.startTagName.location
1495
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1496
            }
1497
            if (!type) {
46✔
1498
                if (!field.alias) {
3✔
1499
                    this.addDiagnostic({
2✔
1500
                        ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'type'),
1501
                        location: field.tokens.startTagName.location
1502
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1503
                }
1504
            } else if (!SGFieldTypes.includes(type.toLowerCase())) {
43✔
1505
                this.addDiagnostic({
1✔
1506
                    ...DiagnosticMessages.xmlInvalidFieldType(type),
1507
                    location: field.getAttribute('type')?.tokens.value.location
3!
1508
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1509
            }
1510
            if (onChange) {
46✔
1511
                if (!callableContainerMap.has(onChange.toLowerCase())) {
1!
1512
                    this.addDiagnostic({
1✔
1513
                        ...DiagnosticMessages.xmlFunctionNotFound(onChange),
1514
                        location: field.getAttribute('onchange')?.tokens.value.location
3!
1515
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1516
                }
1517
            }
1518
        }
1519
    }
1520

1521
    private validateDocComments(node: AstNode) {
1522
        const doc = brsDocParser.parseNode(node);
294✔
1523
        for (const docTag of doc.tags) {
294✔
1524
            const docTypeTag = docTag as BrsDocWithType;
31✔
1525
            if (!docTypeTag.typeExpression || !docTypeTag.location) {
31✔
1526
                continue;
2✔
1527
            }
1528
            const foundType = docTypeTag.typeExpression?.getType({ flags: SymbolTypeFlag.typetime });
29!
1529
            if (!foundType?.isResolvable()) {
29!
1530
                this.addMultiScopeDiagnostic({
8✔
1531
                    ...DiagnosticMessages.cannotFindName(docTypeTag.typeString),
1532
                    location: brsDocParser.getTypeLocationFromToken(docTypeTag.token) ?? docTypeTag.location
24!
1533
                });
1534
            }
1535
        }
1536
    }
1537

1538
    /**
1539
     * Detect when a child has imported a script that an ancestor also imported
1540
     */
1541
    private diagnosticDetectDuplicateAncestorScriptImports(scope: XmlScope) {
1542
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLImports });
544!
1543
        if (scope.xmlFile.parentComponent) {
544✔
1544
            //build a lookup of pkg paths -> FileReference so we can more easily look up collisions
1545
            let parentScriptImports = scope.xmlFile.getAncestorScriptTagImports();
34✔
1546
            let lookup = {} as Record<string, FileReference>;
34✔
1547
            for (let parentScriptImport of parentScriptImports) {
34✔
1548
                //keep the first occurance of a pkgPath. Parent imports are first in the array
1549
                if (!lookup[parentScriptImport.destPath]) {
30!
1550
                    lookup[parentScriptImport.destPath] = parentScriptImport;
30✔
1551
                }
1552
            }
1553

1554
            //add warning for every script tag that this file shares with an ancestor
1555
            for (let scriptImport of scope.xmlFile.scriptTagImports) {
34✔
1556
                let ancestorScriptImport = lookup[scriptImport.destPath];
30✔
1557
                if (ancestorScriptImport) {
30✔
1558
                    let ancestorComponent = ancestorScriptImport.sourceFile as XmlFile;
21✔
1559
                    let ancestorComponentName = ancestorComponent.componentName?.text ?? ancestorComponent.destPath;
21!
1560
                    this.addDiagnostic({
21✔
1561
                        location: util.createLocationFromFileRange(scope.xmlFile, scriptImport.filePathRange),
1562
                        ...DiagnosticMessages.unnecessaryScriptImportInChildFromParent(ancestorComponentName)
1563
                    }, ScopeValidatorDiagnosticTag.XMLImports);
1564
                }
1565
            }
1566
        }
1567
    }
1568

1569
    /**
1570
     * Wraps the AstNode.getType() method, so that we can do extra processing on the result based on the current file
1571
     * In particular, since BrightScript does not support Unions, and there's no way to cast them to something else
1572
     * if the result of .getType() is a union, and we're in a .brs (brightScript) file, treat the result as Dynamic
1573
     *
1574
     * Also, for BrightScript parse-mode, if .getType() returns a node type, do not validate unknown members.
1575
     *
1576
     * In most cases, this returns the result of node.getType()
1577
     *
1578
     * @param file the current file being processed
1579
     * @param node the node to get the type of
1580
     * @param getTypeOpts any options to pass to node.getType()
1581
     * @returns the processed result type
1582
     */
1583
    private getNodeTypeWrapper(file: BrsFile, node: AstNode, getTypeOpts: GetTypeOptions) {
1584
        const type = node?.getType(getTypeOpts);
10,884!
1585

1586
        if (file.parseMode === ParseMode.BrightScript) {
10,884✔
1587
            // this is a brightscript file
1588
            const typeChain = getTypeOpts.typeChain;
1,064✔
1589
            if (typeChain) {
1,064✔
1590
                const hasUnion = typeChain.reduce((hasUnion, tce) => {
360✔
1591
                    return hasUnion || isUnionType(tce.type);
417✔
1592
                }, false);
1593
                if (hasUnion) {
360✔
1594
                    // there was a union somewhere in the typechain
1595
                    return DynamicType.instance;
1✔
1596
                }
1597
            }
1598
            if (isUnionType(type)) {
1,063!
1599
                //this is a union
UNCOV
1600
                return DynamicType.instance;
×
1601
            }
1602

1603
            if (isComponentType(type)) {
1,063✔
1604
                // modify type to allow any member access for Node types
1605
                type.changeUnknownMemberToDynamic = true;
18✔
1606
            }
1607
        }
1608

1609
        // by default return the result of node.getType()
1610
        return type;
10,883✔
1611
    }
1612

1613
    private getParentTypeDescriptor(typeChainResult: TypeChainProcessResult) {
1614
        if (typeChainResult.itemParentTypeKind === BscTypeKind.NamespaceType) {
262✔
1615
            return 'namespace';
119✔
1616
        }
1617
        return 'type';
143✔
1618
    }
1619

1620
    private addDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1621
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
51!
1622
        this.event.program.diagnostics.register(diagnostic, {
51✔
1623
            tags: [diagnosticTag],
1624
            segment: this.currentSegmentBeingValidated
1625
        });
1626
    }
1627

1628
    /**
1629
     * Add a diagnostic (to the first scope) that will have `relatedInformation` for each affected scope
1630
     */
1631
    private addMultiScopeDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1632
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
586✔
1633
        this.event.program.diagnostics.register(diagnostic, {
586✔
1634
            tags: [diagnosticTag],
1635
            segment: this.currentSegmentBeingValidated,
1636
            scope: this.event.scope
1637
        });
1638
    }
1639
}
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