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

rokucommunity / brighterscript / #15219

22 Feb 2026 02:27AM UTC coverage: 87.193% (-0.006%) from 87.199%
#15219

push

web-flow
Merge d0c9a16a7 into 1556715dd

14749 of 17875 branches covered (82.51%)

Branch coverage included in aggregate %.

107 of 117 new or added lines in 19 files covered. (91.45%)

161 existing lines in 16 files now uncovered.

15493 of 16809 relevant lines covered (92.17%)

25604.58 hits per line

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

91.21
/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, isCompoundType, isDottedGetExpression, isDynamicType, isEnumMemberType, isEnumType, isFunctionExpression, isFunctionParameterExpression, isIterableType, isLiteralExpression, isNamespaceStatement, isNamespaceType, isNewExpression, isNumberTypeLike, isObjectType, isPrimitiveType, isReferenceType, isReturnStatement, isStringTypeLike, isTypeStatementType, 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,213✔
73
    private validationKindsMetrics = new Map<string, { timeMs: number; count: number }>();
2,213✔
74

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

92
        logger.time(LogLevel.debug, ['Validating scope', this.event.scope.name], () => {
2,126✔
93
            metrics.fileWalkTime = validationStopwatch.getDurationTextFor(() => {
2,126✔
94
                this.walkFiles();
2,126✔
95
            }).durationText;
96
            this.currentSegmentBeingValidated = null;
2,123✔
97
            metrics.flagDuplicateFunctionTime = validationStopwatch.getDurationTextFor(() => {
2,123✔
98
                this.flagDuplicateFunctionDeclarations();
2,123✔
99
            }).durationText;
100
            metrics.scriptImportValidationTime = validationStopwatch.getDurationTextFor(() => {
2,123✔
101
                this.validateScriptImportPaths();
2,123✔
102
            }).durationText;
103
            metrics.classValidationTime = validationStopwatch.getDurationTextFor(() => {
2,123✔
104
                this.validateClasses();
2,123✔
105
            }).durationText;
106
            metrics.xmlValidationTime = validationStopwatch.getDurationTextFor(() => {
2,123✔
107
                if (isXmlScope(this.event.scope)) {
2,123✔
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,123✔
116
        let totalSegments = 0;
2,123✔
117
        for (const [filePath, metric] of this.segmentsMetrics) {
2,123✔
118
            this.event.program.logger.debug(' - ', filePath, metric.segments, metric.time);
2,056✔
119
            totalSegments += metric.segments;
2,056✔
120
        }
121
        logger.debug(this.event.scope.name, 'total segments validated', totalSegments);
2,123✔
122
        this.logValidationMetrics(metrics);
2,123✔
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,123✔
128
        for (let key in metrics) {
2,123✔
129
            logs.push(`${key}=${chalk.yellow(metrics[key].toString())}`);
10,615✔
130
        }
131
        this.event.program.logger.debug(`Validation Metrics (Scope: ${this.event.scope.name}): ${logs.join(', ')}`);
2,123✔
132
        let kindsLogs = [] as string[];
2,123✔
133
        const kindsArray = Array.from(this.validationKindsMetrics.keys()).sort();
2,123✔
134
        for (let key of kindsArray) {
2,123✔
135
            const timeData = this.validationKindsMetrics.get(key);
7,092✔
136
            kindsLogs.push(`${key}=${chalk.yellow(timeData.timeMs.toFixed(3).toString()) + 'ms'} (${timeData.count})`);
7,092✔
137
        }
138
        this.event.program.logger.debug(`Validation Walk Metrics (Scope: ${this.event.scope.name}): ${kindsLogs.join(', ')}`);
2,123✔
139
    }
140

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

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

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

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

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

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

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

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

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

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

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

181
                const validationVisitor = createVisitor({
2,059✔
182
                    VariableExpression: (varExpr) => {
183
                        this.addValidationKindMetric('VariableExpression', () => {
5,041✔
184
                            this.validateVariableAndDottedGetExpressions(file, varExpr);
5,041✔
185
                        });
186
                    },
187
                    DottedGetExpression: (dottedGet) => {
188
                        this.addValidationKindMetric('DottedGetExpression', () => {
1,706✔
189
                            this.validateVariableAndDottedGetExpressions(file, dottedGet);
1,706✔
190
                        });
191
                    },
192
                    CallExpression: (functionCall) => {
193
                        this.addValidationKindMetric('CallExpression', () => {
1,113✔
194
                            this.validateCallExpression(file, functionCall);
1,113✔
195
                            this.validateCreateObjectCall(file, functionCall);
1,113✔
196
                            this.validateComponentMethods(file, functionCall);
1,113✔
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', () => {
471✔
206
                            this.validateReturnStatement(file, returnStatement);
471✔
207
                        });
208
                    },
209
                    DottedSetStatement: (dottedSetStmt) => {
210
                        this.addValidationKindMetric('DottedSetStatement', () => {
104✔
211
                            this.validateDottedSetStatement(file, dottedSetStmt);
104✔
212
                        });
213
                    },
214
                    BinaryExpression: (binaryExpr) => {
215
                        this.addValidationKindMetric('BinaryExpression', () => {
403✔
216
                            this.validateBinaryExpression(file, binaryExpr);
403✔
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', () => {
841✔
226
                            this.validateAssignmentStatement(file, assignStmt);
841✔
227
                            // Note: this also includes For statements
228
                            this.detectShadowedLocalVar(file, {
841✔
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,523✔
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', () => {
56✔
253
                            this.detectShadowedLocalVar(file, {
56✔
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
168✔
258
                            });
259
                            this.validateForEachStatement(file, forEachStmt);
56✔
260
                        });
261
                    },
262
                    FunctionParameterExpression: (funcParam) => {
263
                        this.addValidationKindMetric('FunctionParameterExpression', () => {
1,399✔
264
                            this.detectShadowedLocalVar(file, {
1,399✔
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
4,197✔
269
                            });
270
                        });
271
                    },
272
                    FunctionExpression: (func) => {
273
                        if (file.isTypedef) {
2,463✔
274
                            return;
10✔
275
                        }
276
                        this.addValidationKindMetric('FunctionExpression', () => {
2,453✔
277
                            this.validateFunctionExpressionForReturn(func);
2,453✔
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) {
31,150✔
288
                            return;
24,239✔
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,059✔
298
                    ? file.validationSegmenter.getAllUnvalidatedSegments()
2,059!
299
                    : file.validationSegmenter.getSegmentsWithChangedSymbols(this.event.changedSymbols);
300

301
                let segmentsValidated = 0;
2,059✔
302

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

308

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

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

344
    private doesFileProvideChangedSymbol(file: BrsFile, changedSymbols: Map<SymbolTypeFlag, Set<string>>) {
345
        if (!changedSymbols) {
64!
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();
5,148✔
366
        return isKnownType;
5,148✔
367
    }
368

369
    private getCircularReference(exprType: BscType) {
370
        if (exprType?.isResolvable()) {
264!
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)) {
5,148✔
387
            return false;
1,249✔
388
        }
389
        let assignmentAncestor: AssignmentStatement;
390
        if (isAssignmentStatement(definingNode) && definingNode.tokens.equals.kind === TokenKind.Equal) {
3,899✔
391
            // this symbol was defined in a "normal" assignment (eg. not a compound assignment)
392
            assignmentAncestor = definingNode;
483✔
393
            return assignmentAncestor?.tokens.name?.text.toLowerCase() === expression?.tokens.name?.text.toLowerCase();
483!
394
        } else if (isFunctionParameterExpression(definingNode)) {
3,416✔
395
            // this symbol was defined in a function param
396
            return true;
710✔
397
        } else {
398
            assignmentAncestor = expression?.findAncestor(isAssignmentStatement);
2,706!
399
        }
400
        return assignmentAncestor?.tokens.name === expression?.tokens.name && isUnionType(exprType);
2,706!
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,113✔
413
        if (callName !== 'createobject' || !isLiteralExpression(call?.args[0])) {
1,113!
414
            return;
1,033✔
415
        }
416
        const firstParamToken = (call?.args[0] as LiteralExpression)?.tokens?.value;
80!
417
        const firstParamStringValue = firstParamToken?.text?.replace(/"/g, '');
80!
418
        if (!firstParamStringValue) {
80!
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!
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,113✔
488
        if (!isDottedGetExpression(call.callee)) {
1,113✔
489
            return;
684✔
490
        }
491

492
        const callName = call.callee.tokens?.name?.text?.toLowerCase();
429!
493
        if (!callName || !lowerMethodNamesChecked.includes(callName) || !isLiteralExpression(call?.args[0])) {
429!
494
            return;
416✔
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,113✔
522
        let funcType = this.getNodeTypeWrapper(file, expression?.callee, getTypeOptions);
1,113!
523
        if (funcType?.isResolvable() && isClassType(funcType)) {
1,113✔
524
            // We're calling a class - get the constructor
525
            funcType = funcType.getMemberType('new', getTypeOptions);
131✔
526
        }
527
        const callErrorLocation = expression?.callee?.location;
1,113!
528
        return this.validateFunctionCall(file, expression.callee, funcType, callErrorLocation, expression.args);
1,113✔
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,151✔
561
        while (isTypeStatementType(funcType)) {
1,155✔
562
            funcType = funcType.wrappedType;
11✔
563
        }
564
        if (!funcType?.isResolvable() || !isCallableType(funcType) || isCompoundType(funcType)) {
1,155✔
565
            const funcName = util.getAllDottedGetPartsAsString(callee, ParseMode.BrighterScript, isCallfuncExpression(callee) ? '@.' : '.');
170✔
566
            if (isUnionType(funcType)) {
170✔
567
                if (!util.isUnionOfFunctions(funcType) && !isCallfuncExpression(callee)) {
10!
568
                    // union of func and non func. not callable
UNCOV
569
                    this.addMultiScopeDiagnostic({
×
570
                        ...DiagnosticMessages.notCallable(funcName),
571
                        location: callErrorLocation
572
                    });
UNCOV
573
                    return;
×
574
                }
575
                const callablesInUnion = funcType.types.filter(isCallableType);
10✔
576
                const funcsInUnion = callablesInUnion.filter(isTypedFunctionType);
10✔
577
                if (funcsInUnion.length < callablesInUnion.length) {
10✔
578
                    // potentially a non-typed func in union
579
                    // cannot validate
580
                    return;
1✔
581
                }
582
                // check all funcs to see if they work
583
                for (let i = 1; i < funcsInUnion.length; i++) {
9✔
584
                    const compatibilityData: TypeCompatibilityData = {};
8✔
585
                    if (!funcsInUnion[0].isTypeCompatible(funcsInUnion[i], compatibilityData)) {
8✔
586
                        if (!compatibilityData.returnTypeMismatch) {
5✔
587
                            // param differences!
588
                            this.addMultiScopeDiagnostic({
2✔
589
                                ...DiagnosticMessages.incompatibleSymbolDefinition(
590
                                    funcName,
591
                                    { isUnion: true, data: compatibilityData }),
592
                                location: callErrorLocation
593
                            });
594
                            return;
2✔
595
                        }
596
                    }
597
                }
598
                // The only thing different was return type
599
                funcType = util.getFunctionTypeFromUnion(funcType);
7✔
600

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

614
            }
615
        }
616

617
        if (!isTypedFunctionType(funcType)) {
1,146✔
618
            // non typed function. nothing to check
619
            return;
293✔
620
        }
621

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

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

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

658
            if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) {
725✔
659
                argType = data.definingNode.getConstructorType();
2✔
660
            }
661

662
            const compatibilityData: TypeCompatibilityData = {};
725✔
663
            const isAllowedArgConversion = this.checkAllowedArgConversions(paramType, argType);
725✔
664
            if (!isAllowedArgConversion && !paramType?.isTypeCompatible(argType, compatibilityData)) {
725!
665
                this.addMultiScopeDiagnostic({
59✔
666
                    ...DiagnosticMessages.argumentTypeMismatch(argType?.toString() ?? 'unknown', paramType?.toString() ?? 'unknown', compatibilityData),
708!
667
                    location: arg.location
668
                });
669
            }
670
            paramIndex++;
725✔
671
        }
672
    }
673

674
    private checkAllowedArgConversions(paramType: BscType, argType: BscType): boolean {
675
        if (isNumberTypeLike(argType) && isBooleanTypeLike(paramType)) {
725✔
676
            return true;
8✔
677
        }
678
        return false;
717✔
679
    }
680

681

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

695
            // `return` statement by itself in non-built-in function will actually result in `invalid`
696
            const valueReturnType = isVoidType(actualReturnType) ? InvalidType.instance : actualReturnType;
471✔
697

698
            if (funcType.returnType.isResolvable()) {
471✔
699
                if (!returnStmt?.value && isVoidType(funcType.returnType)) {
467!
700
                    // allow empty return when function is return `as void`
701
                    // eslint-disable-next-line no-useless-return
702
                    return;
8✔
703
                } else if (!funcType.returnType.isTypeCompatible(valueReturnType, compatibilityData)) {
459✔
704
                    this.addMultiScopeDiagnostic({
32✔
705
                        ...DiagnosticMessages.returnTypeMismatch(actualReturnType.toString(), funcType.returnType.toString(), compatibilityData),
706
                        location: returnStmt.value?.location ?? returnStmt.location
192✔
707
                    });
708
                }
709
            }
710
        }
711
    }
712

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

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

738
        let accessibilityIsOk = this.checkMemberAccessibility(file, dottedSetStmt, typeChainExpectedLHS);
60✔
739

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

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

757
    /**
758
     * Detect when declared type does not match rhs type
759
     */
760
    private validateAssignmentStatement(file: BrsFile, assignStmt: AssignmentStatement) {
761
        if (!assignStmt?.typeExpression) {
841!
762
            // nothing to check
763
            return;
826✔
764
        }
765

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

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

787
        if (util.isInTypeExpression(binaryExpr)) {
470✔
788
            return;
86✔
789
        }
790

791
        let leftType = isBinaryExpression(binaryExpr)
384✔
792
            ? this.getNodeTypeWrapper(file, binaryExpr.left, getTypeOpts)
384✔
793
            : this.getNodeTypeWrapper(file, binaryExpr.item, getTypeOpts);
794
        let rightType = isBinaryExpression(binaryExpr)
384✔
795
            ? this.getNodeTypeWrapper(file, binaryExpr.right, getTypeOpts)
384✔
796
            : this.getNodeTypeWrapper(file, binaryExpr.value, getTypeOpts);
797

798
        if (!leftType || !rightType || !leftType.isResolvable() || !rightType.isResolvable()) {
384✔
799
            // Can not find the type. error handled elsewhere
800
            return;
13✔
801
        }
802

803
        let leftTypeToTest = leftType;
371✔
804
        let rightTypeToTest = rightType;
371✔
805

806
        if (isEnumMemberType(leftType) || isEnumType(leftType)) {
371✔
807
            leftTypeToTest = leftType.underlyingType;
11✔
808
        }
809
        if (isEnumMemberType(rightType) || isEnumType(rightType)) {
371✔
810
            rightTypeToTest = rightType.underlyingType;
10✔
811
        }
812

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

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

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

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

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

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

850
        } else if (isDynamicType(rightTypeToTest) || isObjectType(rightTypeToTest)) {
35✔
851
            // operand is basically "any" type... ignore;
852

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

870
    private validateIncrementStatement(file: BrsFile, incStmt: IncrementStatement) {
871
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
11✔
872

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

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

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

896

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

911
        let symbolType = SymbolTypeFlag.runtime;
5,148✔
912
        let oppositeSymbolType = SymbolTypeFlag.typetime;
5,148✔
913
        const isUsedAsType = util.isInTypeExpression(expression);
5,148✔
914
        if (isUsedAsType) {
5,148✔
915
            // This is used in a TypeExpression - only look up types from SymbolTable
916
            symbolType = SymbolTypeFlag.typetime;
1,819✔
917
            oppositeSymbolType = SymbolTypeFlag.runtime;
1,819✔
918
        }
919

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

930
        const hasValidDeclaration = this.hasValidDeclaration(expression, exprType, typeData?.definingNode);
5,148!
931

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

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

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

997
            }
998
        }
999
        if (isUsedAsType) {
5,148✔
1000
            return;
1,819✔
1001
        }
1002

1003
        const containingNamespace = expression.findAncestor<NamespaceStatement>(isNamespaceStatement);
3,329✔
1004
        const containingNamespaceName = containingNamespace?.getName(ParseMode.BrighterScript);
3,329✔
1005

1006
        if (!(isCallExpression(expression.parent) && isNewExpression(expression.parent?.parent))) {
3,329!
1007
            const classUsedAsVarEntry = this.checkTypeChainForClassUsedAsVar(typeChain, containingNamespaceName);
3,208✔
1008
            const isClassInNamespace = containingNamespace?.getSymbolTable().hasSymbol(typeChain[0].name, SymbolTypeFlag.runtime);
3,208✔
1009
            if (classUsedAsVarEntry && !isClassInNamespace) {
3,208!
1010

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

1019
        const lastTypeInfo = typeChain[typeChain.length - 1];
3,329✔
1020
        const parentTypeInfo = typeChain[typeChain.length - 2];
3,329✔
1021

1022
        this.checkMemberAccessibility(file, expression, typeChain);
3,329✔
1023

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

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

1080
        return classUsedAsVar;
3,208✔
1081
    }
1082

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

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

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

1131
            }
1132
        }
1133
        return true;
3,380✔
1134
    }
1135

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

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

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

1154
            this.addMultiScopeDiagnostic({
8✔
1155
                ...DiagnosticMessages.expressionIsNotConstructable(fullName),
1156
                location: newExpression.className.location
1157
            });
1158

1159
        }
1160
    }
1161

1162
    private validateFunctionExpressionForReturn(func: FunctionExpression) {
1163
        const returnType = func?.returnTypeExpression?.getType({ flags: SymbolTypeFlag.typetime });
2,453!
1164

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

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

1183
        //for each list of callables with the same name
1184
        for (let [lowerName, callableContainers] of this.event.scope.getCallableContainerMap()) {
2,123✔
1185

1186
            let globalCallables = [] as CallableContainer[];
159,423✔
1187
            let nonGlobalCallables = [] as CallableContainer[];
159,423✔
1188
            let ownCallables = [] as CallableContainer[];
159,423✔
1189
            let ancestorNonGlobalCallables = [] as CallableContainer[];
159,423✔
1190

1191

1192
            for (let container of callableContainers) {
159,423✔
1193
                if (container.scope === this.event.program.globalScope) {
165,826✔
1194
                    globalCallables.push(container);
163,471✔
1195
                } else {
1196
                    nonGlobalCallables.push(container);
2,355✔
1197
                    if (container.scope === this.event.scope) {
2,355✔
1198
                        ownCallables.push(container);
2,325✔
1199
                    } else {
1200
                        ancestorNonGlobalCallables.push(container);
30✔
1201
                    }
1202
                }
1203
            }
1204

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

1229
            //add error diagnostics about duplicate functions in the same scope
1230
            if (ownCallables.length > 1) {
159,423✔
1231

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

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

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

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

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

1295
    /**
1296
     * Validate all classes defined in this scope
1297
     */
1298
    private validateClasses() {
1299
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Classes });
2,123✔
1300

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

1310

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

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

1328
                    });
1329
                }
1330
            }
1331
        }
1332
    }
1333

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

1341
        const varIsFunction = () => {
2,296✔
1342
            return isCallableType(varDeclaration.type) && !isDynamicType(varDeclaration.type);
11✔
1343
        };
1344

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

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

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

1437
        let targetItemType: BscType;
1438

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

1455
        const loopType = forEachStmt.getLoopVariableType({ flags: SymbolTypeFlag.runtime, statementIndex: forEachStmt.statementIndex });
44✔
1456
        if (loopType?.isResolvable()) {
44!
1457

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

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

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

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

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

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

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

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

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

1612
        // by default return the result of node.getType()
1613
        return type;
11,310✔
1614
    }
1615

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

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

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