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

rokucommunity / brighterscript / #15903

11 May 2026 06:41PM UTC coverage: 86.896% (-2.2%) from 89.094%
#15903

push

web-flow
Merge 70dfd6181 into ce68f5cb7

15597 of 18958 branches covered (82.27%)

Branch coverage included in aggregate %.

9 of 9 new or added lines in 3 files covered. (100.0%)

955 existing lines in 53 files now uncovered.

16351 of 17808 relevant lines covered (91.82%)

27326.16 hits per line

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

91.5
/src/bscPlugin/validation/ScopeValidator.ts
1
import * as path from 'path';
1✔
2
import { DiagnosticTag, type Range } from 'vscode-languageserver';
1✔
3
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✔
4
import type { DiagnosticInfo } from '../../DiagnosticMessages';
5
import { DiagnosticMessages } from '../../DiagnosticMessages';
1✔
6
import type { BrsFile } from '../../files/BrsFile';
7
import type { BsDiagnostic, CallableContainer, ExtraSymbolData, FileReference, GetTypeOptions, ValidateScopeEvent, TypeChainEntry, TypeChainProcessResult, TypeCompatibilityData } from '../../interfaces';
8
import { SymbolTypeFlag } from '../../SymbolTypeFlag';
1✔
9
import type { AssignmentStatement, AugmentedAssignmentStatement, ClassStatement, ConstStatement, DottedSetStatement, ForEachStatement, ForStatement, IncrementStatement, NamespaceStatement, ReturnStatement } from '../../parser/Statement';
10
import { util } from '../../util';
1✔
11
import { nodes, components } from '../../roku-types';
1✔
12
import type { BRSComponentData } from '../../roku-types';
13
import type { Token } from '../../lexer/Token';
14
import { AstNodeKind } from '../../parser/AstNode';
1✔
15
import type { AstNode } from '../../parser/AstNode';
16
import type { Expression } from '../../parser/AstNode';
17
import type { VariableExpression, DottedGetExpression, BinaryExpression, UnaryExpression, NewExpression, LiteralExpression, FunctionExpression, CallfuncExpression, AAIndexedMemberExpression } from '../../parser/Expression';
18
import { CallExpression } from '../../parser/Expression';
1✔
19
import { createVisitor, WalkMode } from '../../astUtils/visitors';
1✔
20
import type { BscType } from '../../types/BscType';
21
import type { BscFile } from '../../files/BscFile';
22
import { InsideSegmentWalkMode } from '../../AstValidationSegmenter';
1✔
23
import { TokenKind } from '../../lexer/TokenKind';
1✔
24
import { ParseMode } from '../../parser/Parser';
1✔
25
import { BsClassValidator } from '../../validators/ClassValidator';
1✔
26
import { globalCallableMap } from '../../globalCallables';
1✔
27
import type { XmlScope } from '../../XmlScope';
28
import type { XmlFile } from '../../files/XmlFile';
29
import { SGFieldTypes } from '../../parser/SGTypes';
1✔
30
import { DynamicType } from '../../types/DynamicType';
1✔
31
import { BscTypeKind } from '../../types/BscTypeKind';
1✔
32
import type { BrsDocWithType } from '../../parser/BrightScriptDocParser';
33
import brsDocParser from '../../parser/BrightScriptDocParser';
1✔
34
import type { Location } from 'vscode-languageserver';
35
import { InvalidType } from '../../types/InvalidType';
1✔
36
import { VoidType } from '../../types/VoidType';
1✔
37
import { LogLevel } from '../../Logger';
1✔
38
import { Stopwatch } from '../../Stopwatch';
1✔
39
import chalk from 'chalk';
1✔
40
import { IntegerType } from '../../types/IntegerType';
1✔
41
import type { Scope } from '../../Scope';
42

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

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

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

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

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

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

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

131
    // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
132
    private logValidationMetrics(metrics: { [key: string]: number | string }) {
133
        let logs = [] as string[];
2,404✔
134
        for (let key in metrics) {
2,404✔
135
            logs.push(`${key}=${chalk.yellow(metrics[key].toString())}`);
14,424✔
136
        }
137
        this.event.program.logger.debug(`Validation Metrics (Scope: ${this.event.scope.name}): ${logs.join(', ')}`);
2,404✔
138
        let kindsLogs = [] as string[];
2,404✔
139
        const kindsArray = Array.from(this.validationKindsMetrics.keys()).sort();
2,404✔
140
        for (let key of kindsArray) {
2,404✔
141
            const timeData = this.validationKindsMetrics.get(key);
7,837✔
142
            kindsLogs.push(`${key}=${chalk.yellow(timeData.timeMs.toFixed(3).toString()) + 'ms'} (${timeData.count})`);
7,837✔
143
        }
144
        this.event.program.logger.debug(`Validation Walk Metrics (Scope: ${this.event.scope.name}): ${kindsLogs.join(', ')}`);
2,404✔
145
    }
146

147
    public reset() {
148
        this.event = undefined;
2,163✔
149
    }
150

151
    private walkFiles() {
152
        const hasChangeInfo = this.event.changedFiles && this.event.changedSymbols;
2,404✔
153

154
        //do many per-file checks for every file in this (and parent) scopes
155
        this.event.scope.enumerateBrsFiles((file) => {
2,404✔
156
            if (!isBrsFile(file)) {
2,883!
UNCOV
157
                return;
×
158
            }
159

160
            const thisFileHasChanges = this.event.changedFiles.includes(file);
2,883✔
161

162
            if (thisFileHasChanges || this.doesFileProvideChangedSymbol(file, this.event.changedSymbols)) {
2,883✔
163
                this.diagnosticDetectFunctionCollisions(file);
2,855✔
164
            }
165
        });
166
        const fileWalkStopWatch = new Stopwatch();
2,404✔
167

168
        this.event.scope.enumerateOwnFiles((file) => {
2,404✔
169
            if (isBrsFile(file)) {
3,500✔
170

171
                if (this.event.program.diagnostics.shouldFilterFile(file)) {
2,873!
UNCOV
172
                    return;
×
173
                }
174

175
                fileWalkStopWatch.reset();
2,873✔
176
                fileWalkStopWatch.start();
2,873✔
177

178
                const fileUri = util.pathToUri(file.srcPath);
2,873✔
179
                const thisFileHasChanges = this.event.changedFiles.includes(file);
2,873✔
180

181
                const hasUnvalidatedSegments = file.validationSegmenter.hasUnvalidatedSegments();
2,873✔
182

183
                if (hasChangeInfo && !hasUnvalidatedSegments) {
2,873✔
184
                    return;
542✔
185
                }
186

187
                const validationVisitor = createVisitor({
2,331✔
188
                    VariableExpression: (varExpr) => {
189
                        this.addValidationKindMetric('VariableExpression', () => {
5,390✔
190
                            this.validateVariableAndDottedGetExpressions(file, varExpr);
5,390✔
191
                        });
192
                    },
193
                    DottedGetExpression: (dottedGet) => {
194
                        this.addValidationKindMetric('DottedGetExpression', () => {
1,819✔
195
                            this.validateVariableAndDottedGetExpressions(file, dottedGet);
1,819✔
196
                        });
197
                    },
198
                    CallExpression: (functionCall) => {
199
                        this.addValidationKindMetric('CallExpression', () => {
1,217✔
200
                            this.validateCallExpression(file, functionCall);
1,217✔
201
                            this.validateCreateObjectCall(file, functionCall);
1,217✔
202
                            this.validateComponentMethods(file, functionCall);
1,217✔
203
                        });
204
                    },
205
                    CallfuncExpression: (functionCall) => {
206
                        this.addValidationKindMetric('CallfuncExpression', () => {
70✔
207
                            this.validateCallFuncExpression(file, functionCall);
70✔
208
                        });
209
                    },
210
                    ReturnStatement: (returnStatement) => {
211
                        this.addValidationKindMetric('ReturnStatement', () => {
515✔
212
                            this.validateReturnStatement(file, returnStatement);
515✔
213
                        });
214
                    },
215
                    DottedSetStatement: (dottedSetStmt) => {
216
                        this.addValidationKindMetric('DottedSetStatement', () => {
105✔
217
                            this.validateDottedSetStatement(file, dottedSetStmt);
105✔
218
                        });
219
                    },
220
                    BinaryExpression: (binaryExpr) => {
221
                        this.addValidationKindMetric('BinaryExpression', () => {
434✔
222
                            this.validateBinaryExpression(file, binaryExpr);
434✔
223
                        });
224
                    },
225
                    UnaryExpression: (unaryExpr) => {
226
                        this.addValidationKindMetric('UnaryExpression', () => {
36✔
227
                            this.validateUnaryExpression(file, unaryExpr);
36✔
228
                        });
229
                    },
230
                    AssignmentStatement: (assignStmt) => {
231
                        this.addValidationKindMetric('AssignmentStatement', () => {
927✔
232
                            this.validateAssignmentStatement(file, assignStmt);
927✔
233
                            // Note: this also includes For statements
234
                            this.detectShadowedLocalVar(file, {
927✔
235
                                expr: assignStmt,
236
                                name: assignStmt.tokens.name.text,
237
                                type: this.getNodeTypeWrapper(file, assignStmt, { flags: SymbolTypeFlag.runtime }),
238
                                nameRange: assignStmt.tokens.name.location?.range
2,781✔
239
                            });
240
                        });
241
                    },
242
                    AugmentedAssignmentStatement: (binaryExpr) => {
243
                        this.addValidationKindMetric('AugmentedAssignmentStatement', () => {
67✔
244
                            this.validateBinaryExpression(file, binaryExpr);
67✔
245
                        });
246
                    },
247
                    IncrementStatement: (stmt) => {
248
                        this.addValidationKindMetric('IncrementStatement', () => {
11✔
249
                            this.validateIncrementStatement(file, stmt);
11✔
250
                        });
251
                    },
252
                    NewExpression: (newExpr) => {
253
                        this.addValidationKindMetric('NewExpression', () => {
129✔
254
                            this.validateNewExpression(file, newExpr);
129✔
255
                        });
256
                    },
257
                    ForEachStatement: (forEachStmt) => {
258
                        this.addValidationKindMetric('ForEachStatement', () => {
59✔
259
                            this.detectShadowedLocalVar(file, {
59✔
260
                                expr: forEachStmt,
261
                                name: forEachStmt.tokens.item.text,
262
                                type: this.getNodeTypeWrapper(file, forEachStmt, { flags: SymbolTypeFlag.runtime }),
263
                                nameRange: forEachStmt.tokens.item.location?.range
177✔
264
                            });
265
                            this.validateForEachStatement(file, forEachStmt);
59✔
266
                        });
267
                    },
268
                    FunctionParameterExpression: (funcParam) => {
269
                        this.addValidationKindMetric('FunctionParameterExpression', () => {
1,456✔
270
                            this.detectShadowedLocalVar(file, {
1,456✔
271
                                expr: funcParam,
272
                                name: funcParam.tokens.name.text,
273
                                type: this.getNodeTypeWrapper(file, funcParam, { flags: SymbolTypeFlag.runtime }),
274
                                nameRange: funcParam.tokens.name.location?.range
4,368✔
275
                            });
276
                        });
277
                    },
278
                    FunctionExpression: (func) => {
279
                        if (file.isTypedef) {
2,741✔
280
                            return;
10✔
281
                        }
282
                        this.addValidationKindMetric('FunctionExpression', () => {
2,731✔
283
                            this.validateFunctionExpressionForReturn(func);
2,731✔
284
                        });
285
                    },
286
                    ForStatement: (forStmt) => {
287
                        this.addValidationKindMetric('ForStatement', () => {
22✔
288
                            this.validateForStatement(file, forStmt);
22✔
289
                        });
290
                    },
291
                    AAIndexedMemberExpression: (member) => {
292
                        this.addValidationKindMetric('AAIndexedMemberExpression', () => {
23✔
293
                            this.validateAAIndexedMemberExpression(file, member);
23✔
294
                        });
295
                    },
296
                    AstNode: (node) => {
297
                        //check for doc comments
298
                        if (!node.leadingTrivia || node.leadingTrivia.filter(triviaToken => triviaToken.kind === TokenKind.Comment).length === 0) {
33,819✔
299
                            return;
26,220✔
300
                        }
301
                        this.addValidationKindMetric('AstNode', () => {
295✔
302
                            this.validateDocComments(node);
295✔
303
                        });
304
                    }
305
                });
306
                // validate only what's needed in the file
307

308
                const segmentsToWalkForValidation = thisFileHasChanges
2,331✔
309
                    ? file.validationSegmenter.getAllUnvalidatedSegments()
2,331!
310
                    : file.validationSegmenter.getSegmentsWithChangedSymbols(this.event.changedSymbols);
311

312
                let segmentsValidated = 0;
2,331✔
313

314
                if (thisFileHasChanges) {
2,331!
315
                    // clear all ScopeValidatorSegment diagnostics for this file
316
                    this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, tag: ScopeValidatorDiagnosticTag.Segment });
2,331✔
317
                }
318

319

320
                for (const segment of segmentsToWalkForValidation) {
2,331✔
321
                    if (!thisFileHasChanges && !file.validationSegmenter.checkIfSegmentNeedsRevalidation(segment, this.event.changedSymbols)) {
4,373!
UNCOV
322
                        continue;
×
323
                    }
324
                    this.currentSegmentBeingValidated = segment;
4,373✔
325
                    if (!thisFileHasChanges) {
4,373!
326
                        // just clear the affected diagnostics
UNCOV
327
                        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, segment: segment, tag: ScopeValidatorDiagnosticTag.Segment });
×
328
                    }
329
                    segmentsValidated++;
4,373✔
330
                    segment.walk(validationVisitor, {
4,373✔
331
                        walkMode: InsideSegmentWalkMode
332
                    });
333
                    file.markSegmentAsValidated(segment);
4,373✔
334
                    this.currentSegmentBeingValidated = null;
4,373✔
335
                }
336
                fileWalkStopWatch.stop();
2,331✔
337
                const timeString = fileWalkStopWatch.getDurationText();
2,331✔
338
                this.segmentsMetrics.set(file.pkgPath, { segments: segmentsValidated, time: timeString });
2,331✔
339
            }
340
        });
341
    }
342

343
    private addValidationKindMetric(name: string, funcToTime: () => void) {
344
        if (!this.validationKindsMetrics.has(name)) {
15,306✔
345
            this.validationKindsMetrics.set(name, { timeMs: 0, count: 0 });
7,837✔
346
        }
347
        const timeData = this.validationKindsMetrics.get(name);
15,306✔
348
        const validationKindStopWatch = new Stopwatch();
15,306✔
349
        validationKindStopWatch.start();
15,306✔
350
        funcToTime();
15,306✔
351
        validationKindStopWatch.stop();
15,306✔
352
        this.validationKindsMetrics.set(name, { timeMs: timeData.timeMs + validationKindStopWatch.totalMilliseconds, count: timeData.count + 1 });
15,306✔
353
    }
354

355

356
    private validateAAIndexedMemberExpression(file: BrsFile, member: AAIndexedMemberExpression) {
357
        const scope = this.event.scope;
23✔
358
        // Direct string literal (e.g. ["my-key"]) is valid
359
        if (isLiteralExpression(member.key)) {
23✔
360
            if (member.key.tokens.value.kind !== TokenKind.StringLiteral) {
4✔
361
                this.addMultiScopeDiagnostic({
3✔
362
                    ...DiagnosticMessages.computedAAKeyMustBeStringExpression(),
363
                    location: member.key.location
364
                });
365
            }
366
            return;
4✔
367
        }
368
        const parts = util.getAllDottedGetParts(member.key);
19✔
369
        //getAllDottedGetParts returns undefined for keys that aren't dotted-get / variable
370
        //chains (e.g. arithmetic expressions like `[1 + 2]: "value"`). Those are not
371
        //compile-time constants, so the diagnostic still applies.
372
        if (!parts || parts.length === 0) {
19✔
373
            this.addMultiScopeDiagnostic({
1✔
374
                ...DiagnosticMessages.computedPropertyKeyMustBeConstantExpression(),
375
                location: member.key.location
376
            });
377
            return;
1✔
378
        }
379
        const enclosingNamespace = member.key.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript)?.toLowerCase();
18✔
380
        const entityName = parts.map(p => p.text.toLowerCase()).join('.');
29✔
381
        // Check enum member
382
        const memberLink = scope.getEnumMemberFileLink(entityName, enclosingNamespace);
18✔
383
        if (memberLink) {
18✔
384
            const value = memberLink.item.getValue();
9✔
385
            if (!value?.startsWith('"')) {
9!
386
                this.addMultiScopeDiagnostic({
2✔
387
                    ...DiagnosticMessages.computedAAKeyMustBeStringExpression(),
388
                    location: member.key.location
389
                });
390
            }
391
            return;
9✔
392
        }
393
        // Check const — follow the chain to find the root literal type
394
        const constLink = scope.getConstFileLink(entityName, enclosingNamespace);
9✔
395
        if (constLink) {
9✔
396
            if (!this.constResolvesToString(constLink.item.value, enclosingNamespace, scope)) {
7✔
397
                this.addMultiScopeDiagnostic({
4✔
398
                    ...DiagnosticMessages.computedAAKeyMustBeStringExpression(),
399
                    location: member.key.location
400
                });
401
            }
402
            return;
7✔
403
        }
404
        this.addMultiScopeDiagnostic({
2✔
405
            ...DiagnosticMessages.computedPropertyKeyMustBeConstantExpression(),
406
            location: member.key.location
407
        });
408
    }
409

410

411
    /**
412
     * Recursively resolve a const/enum reference to determine if its ultimate value is a string.
413
     * Returns true only if the value is confirmed to be a string.
414
     */
415
    private constResolvesToString(value: Expression, enclosingNamespace: string, scope: Scope, visited = new Set<string>()): boolean {
7✔
416
        if (isLiteralExpression(value)) {
12✔
417
            return value.tokens.value.kind === TokenKind.StringLiteral;
6✔
418
        }
419
        const parts = util.getAllDottedGetParts(value);
6✔
420
        if (parts.length === 0) {
6!
UNCOV
421
            return false;
×
422
        }
423
        const entityName = parts.map(p => p.text.toLowerCase()).join('.');
7✔
424
        if (visited.has(entityName)) {
6✔
425
            return false; // circular reference — cannot confirm string
1✔
426
        }
427
        visited.add(entityName);
5✔
428
        const constLink = scope.getConstFileLink(entityName, enclosingNamespace);
5✔
429
        if (constLink) {
5✔
430
            return this.constResolvesToString(constLink.item.value, enclosingNamespace, scope, visited);
4✔
431
        }
432
        const memberLink = scope.getEnumMemberFileLink(entityName, enclosingNamespace);
1✔
433
        if (memberLink) {
1!
434
            return this.constResolvesToString(memberLink.item.value, enclosingNamespace, scope, visited);
1✔
435
        }
UNCOV
436
        return false;
×
437
    }
438

439

440
    private doesFileProvideChangedSymbol(file: BrsFile, changedSymbols: Map<SymbolTypeFlag, Set<string>>) {
441
        if (!changedSymbols) {
29!
UNCOV
442
            return true;
×
443
        }
444
        for (const flag of [SymbolTypeFlag.runtime, SymbolTypeFlag.typetime]) {
29✔
445
            const providedSymbolKeysFlag = file.providedSymbols.symbolMap.get(flag).keys();
58✔
446
            const changedSymbolSetForFlag = changedSymbols.get(flag);
58✔
447

448
            for (let providedKey of providedSymbolKeysFlag) {
58✔
449
                if (changedSymbolSetForFlag.has(providedKey)) {
49✔
450
                    return true;
1✔
451
                }
452
            }
453
        }
454
        return false;
28✔
455
    }
456

457
    private currentSegmentBeingValidated: AstNode;
458

459

460
    private isTypeKnown(exprType: BscType) {
461
        let isKnownType = exprType?.isResolvable();
5,401✔
462
        return isKnownType;
5,401✔
463
    }
464

465
    private getCircularReference(exprType: BscType) {
466
        if (exprType?.isResolvable()) {
296!
UNCOV
467
            return { isCircularReference: false };
×
468
        }
469
        if (isReferenceType(exprType)) {
296✔
470

471
            const info = exprType.getCircularReferenceInfo();
291✔
472
            return info;
291✔
473
        }
474
        return { isCircularReference: false };
5✔
475
    }
476

477

478
    /**
479
     * If this is the lhs of an assignment, we don't need to flag it as unresolved
480
     */
481
    private hasValidDeclaration(expression: Expression, exprType: BscType, definingNode?: AstNode) {
482
        if (!isVariableExpression(expression)) {
5,401✔
483
            return false;
1,341✔
484
        }
485
        let assignmentAncestor: AssignmentStatement;
486
        if (isAssignmentStatement(definingNode) && definingNode.tokens.equals.kind === TokenKind.Equal) {
4,060✔
487
            // this symbol was defined in a "normal" assignment (eg. not a compound assignment)
488
            assignmentAncestor = definingNode;
503✔
489
            return assignmentAncestor?.tokens.name?.text.toLowerCase() === expression?.tokens.name?.text.toLowerCase();
503!
490
        } else if (isFunctionParameterExpression(definingNode)) {
3,557✔
491
            // this symbol was defined in a function param
492
            return true;
730✔
493
        } else {
494
            assignmentAncestor = expression?.findAncestor(isAssignmentStatement);
2,827!
495
        }
496
        return assignmentAncestor?.tokens.name === expression?.tokens.name && isUnionType(exprType);
2,827!
497
    }
498

499
    /**
500
     * Flag circular references between consts (e.g. const A = B; const B = A, or
501
     * aggregate cycles like const A = { x: B }; const B = { y: A }). Without this
502
     * check, the transpile pass silently emits unresolved refs at the cycle break
503
     * point, producing code that fails at runtime.
504
     *
505
     * Mirrors the existing class-hierarchy circular-reference detection: each const
506
     * starts its own walk, and an N-cycle produces N diagnostics (one rooted at
507
     * each member of the cycle).
508
     */
509
    private detectCircularConstReferences() {
510
        const scope = this.event.scope;
2,404✔
511

512
        const followConstRef = (
2,404✔
513
            expression: Expression,
514
            namespace: string | undefined,
515
            chain: ConstStatement[]
516
        ) => {
517
            const parts = util.splitExpression(expression);
121✔
518
            const processedNames: string[] = [];
121✔
519
            for (const part of parts) {
121✔
520
                if (!isVariableExpression(part) && !isDottedGetExpression(part)) {
222!
UNCOV
521
                    return;
×
522
                }
523
                processedNames.push(part.tokens.name?.text?.toLowerCase());
222!
524
                const link = scope.getConstFileLink(processedNames.join('.'), namespace);
222✔
525
                if (link) {
222✔
526
                    walkConst(link.item, link.file, chain);
97✔
527
                    return;
97✔
528
                }
529
            }
530
        };
531

532
        const walkConst = (
2,404✔
533
            constStatement: ConstStatement,
534
            file: BrsFile,
535
            chain: ConstStatement[]
536
        ) => {
537
            const cycleStart = chain.indexOf(constStatement);
506✔
538
            if (cycleStart >= 0) {
506✔
539
                const items = chain.slice(cycleStart).map(c => c.fullName).concat(constStatement.fullName);
43✔
540
                this.addMultiScopeDiagnostic({
17✔
541
                    ...DiagnosticMessages.circularReferenceDetected(items),
542
                    location: constStatement.tokens.name.location
543
                });
544
                return;
17✔
545
            }
546
            chain.push(constStatement);
489✔
547
            const innerNamespace = constStatement.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
489✔
548
            const value = constStatement.value;
489✔
549
            if (value) {
489!
550
                if (isVariableExpression(value) || isDottedGetExpression(value)) {
489✔
551
                    followConstRef(value, innerNamespace, chain);
57✔
552
                } else {
553
                    value.walk(createVisitor({
432✔
554
                        VariableExpression: (varExpr) => {
555
                            if (isDottedGetExpression(varExpr.parent)) {
64✔
556
                                return;
41✔
557
                            }
558
                            followConstRef(varExpr, innerNamespace, chain);
23✔
559
                        },
560
                        DottedGetExpression: (dottedExpr) => {
561
                            if (isDottedGetExpression(dottedExpr.parent)) {
52✔
562
                                return;
11✔
563
                            }
564
                            followConstRef(dottedExpr, innerNamespace, chain);
41✔
565
                        }
566
                    }), { walkMode: WalkMode.visitExpressionsRecursive });
567
                }
568
            }
569
            chain.pop();
489✔
570
        };
571

572
        for (const [, link] of scope.getConstMap()) {
2,404✔
573
            walkConst(link.item, link.file, []);
409✔
574
        }
575
    }
576

577
    /**
578
     * Validate every function call to `CreateObject`.
579
     * Ideally we would create better type checking/handling for this, but in the mean time, we know exactly
580
     * what these calls are supposed to look like, and this is a very common thing for brs devs to do, so just
581
     * do this manually for now.
582
     */
583
    protected validateCreateObjectCall(file: BrsFile, call: CallExpression) {
584

585
        //skip non CreateObject function calls
586
        const callName = util.getAllDottedGetPartsAsString(call.callee)?.toLowerCase();
1,217✔
587
        if (callName !== 'createobject' || !isLiteralExpression(call?.args[0])) {
1,217!
588
            return;
1,136✔
589
        }
590
        const firstParamToken = (call?.args[0] as LiteralExpression)?.tokens?.value;
81!
591
        const firstParamStringValue = firstParamToken?.text?.replace(/"/g, '');
81!
592
        if (!firstParamStringValue) {
81!
UNCOV
593
            return;
×
594
        }
595
        const firstParamStringValueLower = firstParamStringValue.toLowerCase();
81✔
596

597
        //if this is a `createObject('roSGNode'` call, only support known sg node types
598
        if (firstParamStringValueLower === 'rosgnode' && isLiteralExpression(call?.args[1])) {
81!
599
            const componentName: Token = call?.args[1]?.tokens.value;
33!
600
            this.checkComponentName(componentName);
33✔
601
            if (call?.args.length !== 2) {
33!
602
                // roSgNode should only ever have 2 args in `createObject`
603
                this.addDiagnostic({
1✔
604
                    ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, [2], call?.args.length),
3!
605
                    location: call.location
606
                });
607
            }
608
        } else if (!platformComponentNames.has(firstParamStringValueLower)) {
48✔
609
            this.addDiagnostic({
7✔
610
                ...DiagnosticMessages.unknownBrightScriptComponent(firstParamStringValue),
611
                location: firstParamToken.location
612
            });
613
        } else {
614
            // This is valid brightscript component
615
            // Test for invalid arg counts
616
            const brightScriptComponent: BRSComponentData = components[firstParamStringValueLower];
41✔
617
            // Valid arg counts for createObject are 1+ number of args for constructor
618
            let validArgCounts = brightScriptComponent?.constructors.map(cnstr => cnstr.params.length + 1);
41!
619
            if (validArgCounts.length === 0) {
41✔
620
                // no constructors for this component, so createObject only takes 1 arg
621
                validArgCounts = [1];
6✔
622
            }
623
            if (!validArgCounts.includes(call?.args.length)) {
41!
624
                // Incorrect number of arguments included in `createObject()`
625
                this.addDiagnostic({
4✔
626
                    ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, validArgCounts, call?.args.length),
12!
627
                    location: call.location
628
                });
629
            }
630

631
            // Test for deprecation
632
            if (brightScriptComponent?.isDeprecated) {
41!
UNCOV
633
                this.addDiagnostic({
×
634
                    ...DiagnosticMessages.itemIsDeprecated(firstParamStringValue, brightScriptComponent.deprecatedDescription),
635
                    location: call.location
636
                });
637
            }
638
        }
639

640
    }
641

642
    private checkComponentName(componentName: Token) {
643
        //don't validate any components with a colon in their name (probably component libraries, but regular components can have them too).
644
        if (!componentName || componentName?.text?.includes(':')) {
34!
645
            return;
4✔
646
        }
647
        //add diagnostic for unknown components
648
        const unquotedComponentName = componentName?.text?.replace(/"/g, '');
30!
649
        if (unquotedComponentName && !platformNodeNames.has(unquotedComponentName.toLowerCase()) && !this.event.program.getComponent(unquotedComponentName)) {
30✔
650
            this.addDiagnostic({
4✔
651
                ...DiagnosticMessages.unknownRoSGNode(unquotedComponentName),
652
                location: componentName.location
653
            });
654
        }
655
    }
656

657
    /**
658
     * Validate every method call to `component.callfunc()`, `component.createChild()`, etc.
659
     */
660
    protected validateComponentMethods(file: BrsFile, call: CallExpression) {
661
        const lowerMethodNamesChecked = ['callfunc', 'createchild'];
1,217✔
662
        if (!isDottedGetExpression(call.callee)) {
1,217✔
663
            return;
764✔
664
        }
665

666
        const callName = call.callee.tokens?.name?.text?.toLowerCase();
453!
667
        if (!callName || !lowerMethodNamesChecked.includes(callName) || !isLiteralExpression(call?.args[0])) {
453!
668
            return;
440✔
669
        }
670

671
        const callerType = call.callee.obj?.getType({ flags: SymbolTypeFlag.runtime });
13!
672
        if (!isCallFuncableTypeLike(callerType)) {
13✔
673
            return;
2✔
674
        }
675
        const firstArgToken = call?.args[0]?.tokens.value;
11!
676
        if (callName === 'createchild') {
11✔
677
            this.checkComponentName(firstArgToken);
1✔
678
        } else if (callName === 'callfunc' && !util.isGenericNodeType(callerType)) {
10✔
679
            const funcType = util.getCallFuncType(call, firstArgToken, { flags: SymbolTypeFlag.runtime, ignoreCall: true });
7✔
680
            if (!funcType?.isResolvable()) {
7!
681
                const functionName = firstArgToken.text.replace(/"/g, '');
3✔
682
                const functionFullname = `${callerType.toString()}@.${functionName}`;
3✔
683
                this.addMultiScopeDiagnostic({
3✔
684
                    ...DiagnosticMessages.cannotFindCallFuncFunction(functionName, functionFullname, callerType.toString()),
685
                    location: firstArgToken?.location
9!
686
                });
687
            } else {
688
                this.validateFunctionCall(file, call, funcType, firstArgToken.location, call.args, 1);
4✔
689
            }
690
        }
691
    }
692

693

694
    private validateCallExpression(file: BrsFile, expression: CallExpression) {
695
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: {} };
1,217✔
696
        let funcType = this.getNodeTypeWrapper(file, expression?.callee, getTypeOptions);
1,217!
697
        if (funcType?.isResolvable() && isClassType(funcType)) {
1,217✔
698
            // We're calling a class - get the constructor
699
            funcType = funcType.getMemberType('new', getTypeOptions);
138✔
700
        }
701
        const callErrorLocation = expression?.callee?.location;
1,217!
702
        return this.validateFunctionCall(file, expression.callee, funcType, callErrorLocation, expression.args);
1,217✔
703

704
    }
705

706
    private validateCallFuncExpression(file: BrsFile, expression: CallfuncExpression) {
707
        const callerType = expression.callee?.getType({ flags: SymbolTypeFlag.runtime });
70!
708
        if (isDynamicType(callerType)) {
70✔
709
            return;
22✔
710
        }
711
        const methodToken = expression.tokens.methodName;
48✔
712
        const methodName = methodToken?.text ?? '';
48✔
713
        if (!methodName) {
48✔
714
            return;
5✔
715
        }
716
        const functionFullname = `${callerType.toString()}@.${methodName}`;
43✔
717
        const callErrorLocation = expression.location;
43✔
718
        if (util.isGenericNodeType(callerType) || isObjectType(callerType) || isDynamicType(callerType)) {
43✔
719
            // ignore "general" node
720
            return;
5✔
721
        }
722

723
        const funcType = util.getCallFuncType(expression, methodToken, { flags: SymbolTypeFlag.runtime, ignoreCall: true });
38✔
724
        if (!funcType?.isResolvable()) {
38✔
725
            this.addMultiScopeDiagnostic({
10✔
726
                ...DiagnosticMessages.cannotFindCallFuncFunction(methodName, functionFullname, callerType.toString()),
727
                location: callErrorLocation
728
            });
729
        }
730

731
        return this.validateFunctionCall(file, expression, funcType, callErrorLocation, expression.args);
38✔
732
    }
733

734
    /**
735
     * Detect calls to functions with the incorrect number of parameters, or wrong types of arguments
736
     */
737
    private validateFunctionCall(file: BrsFile, callee: Expression, funcType: BscType, callErrorLocation: Location, args: Expression[], argOffset = 0) {
1,255✔
738
        while (isTypeStatementType(funcType)) {
1,259✔
739
            funcType = funcType.wrappedType;
11✔
740
        }
741
        if (!funcType?.isResolvable() || !isCallableType(funcType) || isCompoundType(funcType)) {
1,259✔
742
            const funcName = util.getAllDottedGetPartsAsString(callee, ParseMode.BrighterScript, isCallfuncExpression(callee) ? '@.' : '.');
204✔
743
            if (isUnionType(funcType)) {
204✔
744
                if (!util.isUnionOfFunctions(funcType) && !isCallfuncExpression(callee)) {
10!
745
                    // union of func and non func. not callable
UNCOV
746
                    this.addMultiScopeDiagnostic({
×
747
                        ...DiagnosticMessages.notCallable(funcName),
748
                        location: callErrorLocation
749
                    });
UNCOV
750
                    return;
×
751
                }
752
                const callablesInUnion = funcType.types.filter(isCallableType);
10✔
753
                const funcsInUnion = callablesInUnion.filter(isTypedFunctionType);
10✔
754
                if (funcsInUnion.length < callablesInUnion.length) {
10✔
755
                    // potentially a non-typed func in union
756
                    // cannot validate
757
                    return;
1✔
758
                }
759
                // check all funcs to see if they work
760
                for (let i = 1; i < funcsInUnion.length; i++) {
9✔
761
                    const compatibilityData: TypeCompatibilityData = {};
8✔
762
                    if (!funcsInUnion[0].isTypeCompatible(funcsInUnion[i], compatibilityData)) {
8✔
763
                        if (!compatibilityData.returnTypeMismatch) {
5✔
764
                            // param differences!
765
                            this.addMultiScopeDiagnostic({
2✔
766
                                ...DiagnosticMessages.incompatibleSymbolDefinition(
767
                                    funcName,
768
                                    { isUnion: true, data: compatibilityData }),
769
                                location: callErrorLocation
770
                            });
771
                            return;
2✔
772
                        }
773
                    }
774
                }
775
                // The only thing different was return type
776
                funcType = util.getFunctionTypeFromUnion(funcType);
7✔
777

778
            }
779
            if (funcType && !isCallableType(funcType) && !isReferenceType(funcType)) {
201✔
780
                const globalFuncWithVarName = globalCallableMap.get(funcName.toLowerCase());
7✔
781
                if (globalFuncWithVarName) {
7✔
782
                    funcType = globalFuncWithVarName.type;
1✔
783
                } else {
784
                    this.addMultiScopeDiagnostic({
6✔
785
                        ...DiagnosticMessages.notCallable(funcName),
786
                        location: callErrorLocation
787
                    });
788
                    return;
6✔
789
                }
790

791
            }
792
        }
793

794
        if (!isTypedFunctionType(funcType)) {
1,250✔
795
            // non typed function. nothing to check
796
            return;
336✔
797
        }
798

799
        //get min/max parameter count for callable
800
        let minParams = 0;
914✔
801
        let maxParams = 0;
914✔
802
        for (let param of funcType.params) {
914✔
803
            maxParams++;
1,192✔
804
            //optional parameters must come last, so we can assume that minParams won't increase once we hit
805
            //the first isOptional
806
            if (param.isOptional !== true) {
1,192✔
807
                minParams++;
674✔
808
            }
809
        }
810
        if (funcType.isVariadic) {
914✔
811
            // function accepts variable number of arguments
812
            maxParams = CallExpression.MaximumArguments;
12✔
813
        }
814
        const argsForCall = argOffset < 1 ? args : args.slice(argOffset);
914✔
815

816
        let expCallArgCount = argsForCall.length;
914✔
817
        if (expCallArgCount > maxParams || expCallArgCount < minParams) {
914✔
818
            let minMaxParamsText = minParams === maxParams ? maxParams + argOffset : `${minParams + argOffset}-${maxParams + argOffset}`;
41✔
819
            this.addMultiScopeDiagnostic({
41✔
820
                ...DiagnosticMessages.mismatchArgumentCount(minMaxParamsText, expCallArgCount + argOffset),
821
                location: callErrorLocation
822
            });
823
        }
824
        let paramIndex = 0;
914✔
825
        for (let arg of argsForCall) {
914✔
826
            const data = {} as ExtraSymbolData;
761✔
827
            let argType = this.getNodeTypeWrapper(file, arg, { flags: SymbolTypeFlag.runtime, data: data });
761✔
828

829
            const paramType = funcType.params[paramIndex]?.type;
761✔
830
            if (!paramType) {
761✔
831
                // unable to find a paramType -- maybe there are more args than params
832
                break;
22✔
833
            }
834

835
            if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) {
739✔
836
                argType = data.definingNode.getConstructorType();
2✔
837
            }
838

839
            const compatibilityData: TypeCompatibilityData = {};
739✔
840
            const isAllowedArgConversion = this.checkAllowedArgConversions(paramType, argType);
739✔
841
            if (!isAllowedArgConversion && !paramType?.isTypeCompatible(argType, compatibilityData)) {
739!
842
                this.addMultiScopeDiagnostic({
59✔
843
                    ...DiagnosticMessages.argumentTypeMismatch(argType?.toString() ?? 'unknown', paramType?.toString() ?? 'unknown', compatibilityData),
708!
844
                    location: arg.location
845
                });
846
            }
847
            paramIndex++;
739✔
848
        }
849
    }
850

851
    private checkAllowedArgConversions(paramType: BscType, argType: BscType): boolean {
852
        if (isNumberTypeLike(argType) && isBooleanTypeLike(paramType)) {
739✔
853
            return true;
8✔
854
        }
855
        return false;
731✔
856
    }
857

858

859
    /**
860
     * Detect return statements with incompatible types vs. declared return type
861
     */
862
    private validateReturnStatement(file: BrsFile, returnStmt: ReturnStatement) {
863
        const data: ExtraSymbolData = {};
516✔
864
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: data };
516✔
865
        let funcType = returnStmt.findAncestor(isFunctionExpression)?.getType({ flags: SymbolTypeFlag.typetime });
516✔
866
        if (isTypedFunctionType(funcType)) {
516✔
867
            let actualReturnType = returnStmt?.value
515!
868
                ? this.getNodeTypeWrapper(file, returnStmt?.value, getTypeOptions)
1,973!
869
                : VoidType.instance;
870
            const compatibilityData: TypeCompatibilityData = {};
515✔
871

872
            // `return` statement by itself in non-built-in function will actually result in `invalid`
873
            const valueReturnType = isVoidType(actualReturnType) ? InvalidType.instance : actualReturnType;
515✔
874

875
            if (funcType.returnType.isResolvable()) {
515✔
876
                if (!returnStmt?.value && isVoidType(funcType.returnType)) {
511!
877
                    // allow empty return when function is return `as void`
878
                    // eslint-disable-next-line no-useless-return
879
                    return;
8✔
880
                } else if (!funcType.returnType.isTypeCompatible(valueReturnType, compatibilityData)) {
503✔
881
                    this.addMultiScopeDiagnostic({
46✔
882
                        ...DiagnosticMessages.returnTypeMismatch(actualReturnType.toString(), funcType.returnType.toString(), compatibilityData),
883
                        location: returnStmt.value?.location ?? returnStmt.location
276✔
884
                    });
885
                }
886
            }
887
        }
888
    }
889

890
    /**
891
     * Detect assigned type different from expected member type
892
     */
893
    private validateDottedSetStatement(file: BrsFile, dottedSetStmt: DottedSetStatement) {
894
        const typeChainExpectedLHS = [] as TypeChainEntry[];
105✔
895
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
105✔
896

897
        const expectedLHSType = this.getNodeTypeWrapper(file, dottedSetStmt, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
105✔
898
        const actualRHSType = this.getNodeTypeWrapper(file, dottedSetStmt?.value, getTypeOpts);
105!
899
        const compatibilityData: TypeCompatibilityData = {};
105✔
900
        const typeChainScan = util.processTypeChain(typeChainExpectedLHS);
105✔
901
        // check if anything in typeChain is an AA - if so, just allow it
902
        if (typeChainExpectedLHS.find(typeChainItem => isAssociativeArrayType(typeChainItem.type))) {
185✔
903
            // something in the chain is an AA
904
            // treat members as dynamic - they could have been set without the type system's knowledge
905
            return;
39✔
906
        }
907
        if (!expectedLHSType || !expectedLHSType?.isResolvable()) {
66!
908
            this.addMultiScopeDiagnostic({
5✔
909
                ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
910
                location: typeChainScan?.location
15!
911
            });
912
            return;
5✔
913
        }
914

915
        let accessibilityIsOk = this.checkMemberAccessibility(file, dottedSetStmt, typeChainExpectedLHS);
61✔
916

917
        //Most Component fields can be set with strings
918
        //TODO: be more precise about which fields can actually accept strings
919
        //TODO: if RHS is a string literal, we can do more validation to make sure it's the correct type
920
        if (isComponentType(dottedSetStmt.obj?.getType({ flags: SymbolTypeFlag.runtime }))) {
61!
921
            if (isStringTypeLike(actualRHSType)) {
22✔
922
                return;
7✔
923
            }
924
        }
925

926
        if (accessibilityIsOk && !expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
54!
927
            this.addMultiScopeDiagnostic({
13✔
928
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType?.toString() ?? 'unknown', expectedLHSType?.toString() ?? 'unknown', compatibilityData),
156!
929
                location: dottedSetStmt.location
930
            });
931
        }
932
    }
933

934
    /**
935
     * Detect when declared type does not match rhs type
936
     */
937
    private validateAssignmentStatement(file: BrsFile, assignStmt: AssignmentStatement) {
938
        if (!assignStmt?.typeExpression) {
927!
939
            // nothing to check
940
            return;
911✔
941
        }
942

943
        const typeChainExpectedLHS = [];
16✔
944
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
16✔
945
        const expectedLHSType = this.getNodeTypeWrapper(file, assignStmt.typeExpression, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
16✔
946
        const actualRHSType = this.getNodeTypeWrapper(file, assignStmt.value, getTypeOpts);
16✔
947
        const compatibilityData: TypeCompatibilityData = {};
16✔
948
        if (!expectedLHSType || !expectedLHSType.isResolvable()) {
16✔
949
            // LHS is not resolvable... handled elsewhere
950
        } else if (!expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
15!
951
            this.addMultiScopeDiagnostic({
2✔
952
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
953
                location: assignStmt.location
954
            });
955
        }
956
    }
957

958
    /**
959
     * Detect invalid use of a binary operator
960
     */
961
    private validateBinaryExpression(file: BrsFile, binaryExpr: BinaryExpression | AugmentedAssignmentStatement) {
962
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
501✔
963

964
        if (util.isInTypeExpression(binaryExpr)) {
501✔
965
            return;
110✔
966
        }
967

968
        let leftType = isBinaryExpression(binaryExpr)
391✔
969
            ? this.getNodeTypeWrapper(file, binaryExpr.left, getTypeOpts)
391✔
970
            : this.getNodeTypeWrapper(file, binaryExpr.item, getTypeOpts);
971
        let rightType = isBinaryExpression(binaryExpr)
391✔
972
            ? this.getNodeTypeWrapper(file, binaryExpr.right, getTypeOpts)
391✔
973
            : this.getNodeTypeWrapper(file, binaryExpr.value, getTypeOpts);
974

975
        if (!leftType || !rightType || !leftType.isResolvable() || !rightType.isResolvable()) {
391✔
976
            // Can not find the type. error handled elsewhere
977
            return;
13✔
978
        }
979

980
        let leftTypeToTest = leftType;
378✔
981
        let rightTypeToTest = rightType;
378✔
982

983
        if (isEnumMemberType(leftType) || isEnumType(leftType)) {
378✔
984
            leftTypeToTest = leftType.underlyingType;
11✔
985
        }
986
        if (isEnumMemberType(rightType) || isEnumType(rightType)) {
378✔
987
            rightTypeToTest = rightType.underlyingType;
10✔
988
        }
989

990
        if (isUnionType(leftType) || isUnionType(rightType)) {
378✔
991
            // TODO: it is possible to validate based on innerTypes, but more complicated
992
            // Because you need to verify each combination of types
993
            return;
7✔
994
        }
995
        const opResult = util.binaryOperatorResultType(leftTypeToTest, binaryExpr.tokens.operator, rightTypeToTest);
371✔
996

997
        if (!opResult) {
371✔
998
            // if the result was dynamic or void, that means there wasn't a valid operation
999
            this.addMultiScopeDiagnostic({
10✔
1000
                ...DiagnosticMessages.operatorTypeMismatch(binaryExpr.tokens.operator.text, leftType.toString(), rightType.toString()),
1001
                location: binaryExpr.location
1002
            });
1003
        }
1004
    }
1005

1006
    /**
1007
     * Detect invalid use of a Unary operator
1008
     */
1009
    private validateUnaryExpression(file: BrsFile, unaryExpr: UnaryExpression) {
1010
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
36✔
1011

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

1014
        if (!rightType.isResolvable()) {
36!
1015
            // Can not find the type. error handled elsewhere
UNCOV
1016
            return;
×
1017
        }
1018
        let rightTypeToTest = rightType;
36✔
1019
        if (isEnumMemberType(rightType)) {
36!
UNCOV
1020
            rightTypeToTest = rightType.underlyingType;
×
1021
        }
1022

1023
        if (isUnionType(rightTypeToTest)) {
36✔
1024
            // TODO: it is possible to validate based on innerTypes, but more complicated
1025
            // Because you need to verify each combination of types
1026

1027
        } else if (isDynamicType(rightTypeToTest) || isObjectType(rightTypeToTest)) {
35✔
1028
            // operand is basically "any" type... ignore;
1029

1030
        } else if (isPrimitiveType(rightType)) {
31!
1031
            const opResult = util.unaryOperatorResultType(unaryExpr.tokens.operator, rightTypeToTest);
31✔
1032
            if (!opResult) {
31✔
1033
                this.addMultiScopeDiagnostic({
1✔
1034
                    ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
1035
                    location: unaryExpr.location
1036
                });
1037
            }
1038
        } else {
1039
            // rhs is not a primitive, so no binary operator is allowed
UNCOV
1040
            this.addMultiScopeDiagnostic({
×
1041
                ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
1042
                location: unaryExpr.location
1043
            });
1044
        }
1045
    }
1046

1047
    private validateIncrementStatement(file: BrsFile, incStmt: IncrementStatement) {
1048
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
11✔
1049

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

1052
        if (!rightType.isResolvable()) {
11!
1053
            // Can not find the type. error handled elsewhere
UNCOV
1054
            return;
×
1055
        }
1056

1057
        if (isUnionType(rightType)) {
11!
1058
            // TODO: it is possible to validate based on innerTypes, but more complicated
1059
            // because you need to verify each combination of types
1060
        } else if (isDynamicType(rightType) || isObjectType(rightType)) {
11✔
1061
            // operand is basically "any" type... ignore
1062
        } else if (isNumberTypeLike(rightType)) {
10✔
1063
            // operand is a number.. this is ok
1064
        } else {
1065
            // rhs is not a number, so no increment operator is not allowed
1066
            this.addMultiScopeDiagnostic({
1✔
1067
                ...DiagnosticMessages.operatorTypeMismatch(incStmt.tokens.operator.text, rightType.toString()),
1068
                location: incStmt.location
1069
            });
1070
        }
1071
    }
1072

1073

1074
    validateVariableAndDottedGetExpressions(file: BrsFile, expression: VariableExpression | DottedGetExpression) {
1075
        if (isDottedGetExpression(expression.parent)) {
7,209✔
1076
            // We validate dottedGetExpressions at the top-most level
1077
            return;
1,700✔
1078
        }
1079
        if (isVariableExpression(expression)) {
5,509✔
1080
            if (isAssignmentStatement(expression.parent) && expression.parent.tokens.name === expression.tokens.name) {
4,168!
1081
                // Don't validate LHS of assignments
UNCOV
1082
                return;
×
1083
            } else if (isNamespaceStatement(expression.parent)) {
4,168✔
1084
                return;
3✔
1085
            }
1086
        }
1087

1088
        let symbolType = SymbolTypeFlag.runtime;
5,506✔
1089
        let oppositeSymbolType = SymbolTypeFlag.typetime;
5,506✔
1090
        const isUsedAsType = util.isInTypeExpression(expression);
5,506✔
1091
        if (isUsedAsType) {
5,506✔
1092
            // This is used in a TypeExpression - only look up types from SymbolTable
1093
            symbolType = SymbolTypeFlag.typetime;
1,966✔
1094
            oppositeSymbolType = SymbolTypeFlag.runtime;
1,966✔
1095

1096
            //roku built-in type names (rosgnode*, ifArray, etc.) aren't tracked in any
1097
            //symbol table; skip cannot-find-name validation when used as a type.
1098
            if (isVariableExpression(expression) && util.isBuiltInType(expression.tokens.name.text)) {
1,966✔
1099
                return;
105✔
1100
            }
1101
        }
1102

1103
        // Do a complete type check on all DottedGet and Variable expressions
1104
        // this will create a diagnostic if an invalid member is accessed
1105
        const typeChain: TypeChainEntry[] = [];
5,401✔
1106
        const typeData = {} as ExtraSymbolData;
5,401✔
1107
        let exprType = this.getNodeTypeWrapper(file, expression, {
5,401✔
1108
            flags: symbolType,
1109
            typeChain: typeChain,
1110
            data: typeData
1111
        });
1112

1113
        const hasValidDeclaration = this.hasValidDeclaration(expression, exprType, typeData?.definingNode);
5,401!
1114

1115
        //include a hint diagnostic if this type is marked as deprecated
1116
        if (typeData.flags & SymbolTypeFlag.deprecated) { // eslint-disable-line no-bitwise
5,401✔
1117
            this.addMultiScopeDiagnostic({
2✔
1118
                ...DiagnosticMessages.itemIsDeprecated(),
1119
                location: expression.tokens.name.location,
1120
                tags: [DiagnosticTag.Deprecated]
1121
            });
1122
        }
1123

1124
        if (!this.isTypeKnown(exprType) && !hasValidDeclaration) {
5,401✔
1125
            if (this.getNodeTypeWrapper(file, expression, { flags: oppositeSymbolType, isExistenceTest: true })?.isResolvable()) {
303!
1126
                const oppoSiteTypeChain = [];
5✔
1127
                const invalidlyUsedResolvedType = this.getNodeTypeWrapper(file, expression, { flags: oppositeSymbolType, typeChain: oppoSiteTypeChain, isExistenceTest: true });
5✔
1128
                const typeChainScan = util.processTypeChain(oppoSiteTypeChain);
5✔
1129
                if (isUsedAsType) {
5✔
1130
                    this.addMultiScopeDiagnostic({
2✔
1131
                        ...DiagnosticMessages.itemCannotBeUsedAsType(typeChainScan.fullChainName),
1132
                        location: expression.location
1133
                    });
1134
                } else if (invalidlyUsedResolvedType && !isReferenceType(invalidlyUsedResolvedType)) {
3✔
1135
                    if (!isAliasStatement(expression.parent)) {
1!
1136
                        // alias rhs CAN be a type!
UNCOV
1137
                        this.addMultiScopeDiagnostic({
×
1138
                            ...DiagnosticMessages.itemCannotBeUsedAsVariable(invalidlyUsedResolvedType.toString()),
1139
                            location: expression.location
1140
                        });
1141
                    }
1142
                } else {
1143
                    const typeChainScan = util.processTypeChain(typeChain);
2✔
1144
                    //if this is a function call, provide a different diagnostic code
1145
                    if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
2✔
1146
                        this.addMultiScopeDiagnostic({
1✔
1147
                            ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
1148
                            location: typeChainScan?.location
3!
1149
                        });
1150
                    } else {
1151
                        this.addMultiScopeDiagnostic({
1✔
1152
                            ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
1153
                            location: typeChainScan?.location
3!
1154
                        });
1155
                    }
1156
                }
1157

1158
            } else if (!(typeData?.isFromDocComment)) {
298!
1159
                // only show "cannot find... " errors if the type is not defined from a doc comment
1160
                const typeChainScan = util.processTypeChain(typeChain);
296✔
1161
                const circularReferenceInfo = this.getCircularReference(exprType);
296✔
1162
                if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
296✔
1163
                    this.addMultiScopeDiagnostic({
53✔
1164
                        ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
1165
                        location: typeChainScan?.location
159!
1166
                    });
1167
                } else if (circularReferenceInfo?.isCircularReference) {
243!
1168
                    let diagnosticDetail = util.getCircularReferenceDiagnosticDetail(circularReferenceInfo, typeChainScan.fullNameOfItem);
14✔
1169
                    this.addMultiScopeDiagnostic({
14✔
1170
                        ...DiagnosticMessages.circularReferenceDetected(diagnosticDetail),
1171
                        location: typeChainScan?.location
42!
1172
                    });
1173
                } else {
1174
                    this.addMultiScopeDiagnostic({
229✔
1175
                        ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
1176
                        location: typeChainScan?.location
687!
1177
                    });
1178
                }
1179

1180
            }
1181
        }
1182
        if (isUsedAsType) {
5,401✔
1183
            return;
1,861✔
1184
        }
1185

1186
        const containingNamespace = expression.findAncestor<NamespaceStatement>(isNamespaceStatement);
3,540✔
1187
        const containingNamespaceName = containingNamespace?.getName(ParseMode.BrighterScript);
3,540✔
1188

1189
        if (!(isCallExpression(expression.parent) && isNewExpression(expression.parent?.parent))) {
3,540!
1190
            const classUsedAsVarEntry = this.checkTypeChainForClassUsedAsVar(typeChain, containingNamespaceName);
3,411✔
1191
            const isClassInNamespace = containingNamespace?.getSymbolTable().hasSymbol(typeChain[0].name, SymbolTypeFlag.runtime);
3,411✔
1192
            if (classUsedAsVarEntry && !isClassInNamespace) {
3,411!
1193

UNCOV
1194
                this.addMultiScopeDiagnostic({
×
1195
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable(classUsedAsVarEntry.toString()),
1196
                    location: expression.location
1197
                });
UNCOV
1198
                return;
×
1199
            }
1200
        }
1201

1202
        const lastTypeInfo = typeChain[typeChain.length - 1];
3,540✔
1203
        const parentTypeInfo = typeChain[typeChain.length - 2];
3,540✔
1204

1205
        this.checkMemberAccessibility(file, expression, typeChain);
3,540✔
1206

1207
        if (isNamespaceType(exprType) && !isAliasStatement(expression.parent)) {
3,540✔
1208
            this.addMultiScopeDiagnostic({
24✔
1209
                ...DiagnosticMessages.itemCannotBeUsedAsVariable('namespace'),
1210
                location: expression.location
1211
            });
1212
        } else if (isEnumType(exprType) && !isAliasStatement(expression.parent)) {
3,516✔
1213
            const enumStatement = this.event.scope.getEnum(util.getAllDottedGetPartsAsString(expression));
27✔
1214
            if (enumStatement) {
27✔
1215
                // there's an enum with this name
1216
                this.addMultiScopeDiagnostic({
4✔
1217
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable('enum'),
1218
                    location: expression.location
1219
                });
1220
            }
1221
        } else if (isDynamicType(exprType) && isEnumType(parentTypeInfo?.type) && isDottedGetExpression(expression)) {
3,489✔
1222
            const enumFileLink = this.event.scope.getEnumFileLink(util.getAllDottedGetPartsAsString(expression.obj));
12✔
1223
            const typeChainScanForItem = util.processTypeChain(typeChain);
12✔
1224
            const typeChainScanForParent = util.processTypeChain(typeChain.slice(0, -1));
12✔
1225
            if (enumFileLink) {
12✔
1226
                this.addMultiScopeDiagnostic({
8✔
1227
                    ...DiagnosticMessages.cannotFindName(lastTypeInfo?.name, typeChainScanForItem.fullChainName, typeChainScanForParent.fullNameOfItem, 'enum'),
24!
1228
                    location: lastTypeInfo?.location,
24!
1229
                    relatedInformation: [{
1230
                        message: 'Enum declared here',
1231
                        location: util.createLocationFromRange(
1232
                            util.pathToUri(enumFileLink?.file.srcPath),
24!
1233
                            enumFileLink?.item?.tokens.name.location?.range
72!
1234
                        )
1235
                    }]
1236
                });
1237
            }
1238
        }
1239
    }
1240

1241
    private checkTypeChainForClassUsedAsVar(typeChain: TypeChainEntry[], containingNamespaceName: string) {
1242
        const ignoreKinds = [AstNodeKind.TypecastExpression, AstNodeKind.NewExpression];
3,411✔
1243
        let lowerNameSoFar = '';
3,411✔
1244
        let classUsedAsVar;
1245
        let isFirst = true;
3,411✔
1246
        for (let i = 0; i < typeChain.length - 1; i++) { // do not look at final entry - we CAN use the constructor as a variable
3,411✔
1247
            const tce = typeChain[i];
1,567✔
1248
            lowerNameSoFar += `${lowerNameSoFar ? '.' : ''}${tce.name.toLowerCase()}`;
1,567✔
1249
            if (!isNamespaceType(tce.type)) {
1,567✔
1250
                if (isFirst && containingNamespaceName) {
821✔
1251
                    lowerNameSoFar = `${containingNamespaceName.toLowerCase()}.${lowerNameSoFar}`;
83✔
1252
                }
1253
                if (!tce.astNode || ignoreKinds.includes(tce.astNode.kind)) {
821✔
1254
                    break;
22✔
1255
                } else if (isClassType(tce.type) && lowerNameSoFar.toLowerCase() === tce.type.name.toLowerCase()) {
799✔
1256
                    classUsedAsVar = tce.type;
1✔
1257
                }
1258
                break;
799✔
1259
            }
1260
            isFirst = false;
746✔
1261
        }
1262

1263
        return classUsedAsVar;
3,411✔
1264
    }
1265

1266
    /**
1267
     * Adds diagnostics for accibility mismatches
1268
     *
1269
     * @param file file
1270
     * @param expression containing expression
1271
     * @param typeChain type chain to check
1272
     * @returns true if member accesiibility is okay
1273
     */
1274
    private checkMemberAccessibility(file: BscFile, expression: Expression, typeChain: TypeChainEntry[]) {
1275
        for (let i = 0; i < typeChain.length - 1; i++) {
3,601✔
1276
            const parentChainItem = typeChain[i];
1,768✔
1277
            const childChainItem = typeChain[i + 1];
1,768✔
1278
            if (isClassType(parentChainItem.type)) {
1,768✔
1279
                const containingClassStmt = expression.findAncestor<ClassStatement>(isClassStatement);
165✔
1280
                const classStmtThatDefinesChildMember = childChainItem.data?.definingNode?.findAncestor<ClassStatement>(isClassStatement);
165!
1281
                if (classStmtThatDefinesChildMember) {
165✔
1282
                    const definingClassName = classStmtThatDefinesChildMember.getName(ParseMode.BrighterScript);
159✔
1283
                    const inMatchingClassStmt = containingClassStmt?.getName(ParseMode.BrighterScript).toLowerCase() === parentChainItem.type.name.toLowerCase();
159✔
1284
                    // eslint-disable-next-line no-bitwise
1285
                    if (childChainItem.data.flags & SymbolTypeFlag.private) {
159✔
1286
                        if (!inMatchingClassStmt || childChainItem.data.memberOfAncestor) {
16✔
1287
                            this.addMultiScopeDiagnostic({
4✔
1288
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
1289
                                location: expression.location
1290
                            });
1291
                            // there's an error... don't worry about the rest of the chain
1292
                            return false;
4✔
1293
                        }
1294
                    }
1295

1296
                    // eslint-disable-next-line no-bitwise
1297
                    if (childChainItem.data.flags & SymbolTypeFlag.protected) {
155✔
1298
                        const containingClassName = containingClassStmt?.getName(ParseMode.BrighterScript);
13✔
1299
                        const containingNamespaceName = expression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
13✔
1300
                        const ancestorClasses = this.event.scope.getClassHierarchy(containingClassName, containingNamespaceName).map(link => link.item);
13✔
1301
                        const isSubClassOfDefiningClass = ancestorClasses.includes(classStmtThatDefinesChildMember);
13✔
1302

1303
                        if (!isSubClassOfDefiningClass) {
13✔
1304
                            this.addMultiScopeDiagnostic({
5✔
1305
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
1306
                                location: expression.location
1307
                            });
1308
                            // there's an error... don't worry about the rest of the chain
1309
                            return false;
5✔
1310
                        }
1311
                    }
1312
                }
1313

1314
            }
1315
        }
1316
        return true;
3,592✔
1317
    }
1318

1319
    /**
1320
     * Find all "new" statements in the program,
1321
     * and make sure we can find a class with that name
1322
     */
1323
    private validateNewExpression(file: BrsFile, newExpression: NewExpression) {
1324
        const newExprType = this.getNodeTypeWrapper(file, newExpression, { flags: SymbolTypeFlag.typetime });
129✔
1325
        if (isClassType(newExprType)) {
129✔
1326
            return;
120✔
1327
        }
1328

1329
        let potentialClassName = newExpression.className.getName(ParseMode.BrighterScript);
9✔
1330
        const namespaceName = newExpression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
9!
1331
        let newableClass = this.event.scope.getClass(potentialClassName, namespaceName);
9✔
1332

1333
        if (!newableClass) {
9!
1334
            //try and find functions with this name.
1335
            let fullName = util.getFullyQualifiedClassName(potentialClassName, namespaceName);
9✔
1336

1337
            this.addMultiScopeDiagnostic({
9✔
1338
                ...DiagnosticMessages.expressionIsNotConstructable(fullName),
1339
                location: newExpression.className.location
1340
            });
1341

1342
        }
1343
    }
1344

1345
    private validateFunctionExpressionForReturn(func: FunctionExpression) {
1346
        const returnType = func?.returnTypeExpression?.getType({ flags: SymbolTypeFlag.typetime });
2,731!
1347

1348
        if (!returnType || !returnType.isResolvable() || isVoidType(returnType) || isDynamicType(returnType)) {
2,731✔
1349
            return;
2,404✔
1350
        }
1351
        const returns = func.body?.findChild<ReturnStatement>(isReturnStatement, { walkMode: WalkMode.visitAll });
327!
1352
        if (!returns && isStringTypeLike(returnType)) {
327✔
1353
            this.addMultiScopeDiagnostic({
5✔
1354
                ...DiagnosticMessages.returnTypeCoercionMismatch(returnType.toString()),
1355
                location: func.location
1356
            });
1357
        }
1358
    }
1359

1360
    /**
1361
     * Create diagnostics for any duplicate function declarations
1362
     */
1363
    private flagDuplicateFunctionDeclarations() {
1364
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration });
2,404✔
1365

1366
        //for each list of callables with the same name
1367
        for (let [lowerName, callableContainers] of this.event.scope.getCallableContainerMap()) {
2,404✔
1368

1369
            let globalCallables = [] as CallableContainer[];
180,502✔
1370
            let nonGlobalCallables = [] as CallableContainer[];
180,502✔
1371
            let ownCallables = [] as CallableContainer[];
180,502✔
1372
            let ancestorNonGlobalCallables = [] as CallableContainer[];
180,502✔
1373

1374

1375
            for (let container of callableContainers) {
180,502✔
1376
                if (container.scope === this.event.program.globalScope) {
187,749✔
1377
                    globalCallables.push(container);
185,108✔
1378
                } else {
1379
                    nonGlobalCallables.push(container);
2,641✔
1380
                    if (container.scope === this.event.scope) {
2,641✔
1381
                        ownCallables.push(container);
2,611✔
1382
                    } else {
1383
                        ancestorNonGlobalCallables.push(container);
30✔
1384
                    }
1385
                }
1386
            }
1387

1388
            //add info diagnostics about child shadowing parent functions
1389
            if (ownCallables.length > 0 && ancestorNonGlobalCallables.length > 0) {
180,502✔
1390
                for (let container of ownCallables) {
24✔
1391
                    //skip the init function (because every component will have one of those){
1392
                    if (lowerName !== 'init') {
24✔
1393
                        let shadowedCallable = ancestorNonGlobalCallables[ancestorNonGlobalCallables.length - 1];
23✔
1394
                        if (!!shadowedCallable && shadowedCallable.callable.file === container.callable.file) {
23✔
1395
                            //same file: skip redundant imports
1396
                            continue;
20✔
1397
                        }
1398
                        this.addMultiScopeDiagnostic({
3✔
1399
                            ...DiagnosticMessages.overridesAncestorFunction(
1400
                                container.callable.name,
1401
                                container.scope.name,
1402
                                shadowedCallable.callable.file.destPath,
1403
                                //grab the last item in the list, which should be the closest ancestor's version
1404
                                shadowedCallable.scope.name
1405
                            ),
1406
                            location: util.createLocationFromFileRange(container.callable.file, container.callable.nameRange)
1407
                        }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
1408
                    }
1409
                }
1410
            }
1411

1412
            //add error diagnostics about duplicate functions in the same scope
1413
            if (ownCallables.length > 1) {
180,502✔
1414

1415
                for (let callableContainer of ownCallables) {
6✔
1416
                    let callable = callableContainer.callable;
12✔
1417
                    const related = [];
12✔
1418
                    for (const ownCallable of ownCallables) {
12✔
1419
                        const thatNameRange = ownCallable.callable.nameRange;
24✔
1420
                        if (ownCallable.callable.nameRange !== callable.nameRange) {
24✔
1421
                            related.push({
12✔
1422
                                message: `Function declared here`,
1423
                                location: util.createLocationFromRange(
1424
                                    util.pathToUri(ownCallable.callable.file?.srcPath),
36!
1425
                                    thatNameRange
1426
                                )
1427
                            });
1428
                        }
1429
                    }
1430

1431
                    this.addMultiScopeDiagnostic({
12✔
1432
                        ...DiagnosticMessages.duplicateFunctionImplementation(callable.name),
1433
                        location: util.createLocationFromFileRange(callable.file, callable.nameRange),
1434
                        relatedInformation: related
1435
                    }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
1436
                }
1437
            }
1438
        }
1439
    }
1440

1441
    /**
1442
     * Verify that all of the scripts imported by each file in this scope actually exist, and have the correct case
1443
     */
1444
    private validateScriptImportPaths() {
1445
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Imports });
2,404✔
1446

1447
        let scriptImports = this.event.scope.getOwnScriptImports();
2,404✔
1448
        //verify every script import
1449
        for (let scriptImport of scriptImports) {
2,404✔
1450
            let referencedFile = this.event.scope.getFileByRelativePath(scriptImport.destPath);
740✔
1451
            //if we can't find the file
1452
            if (!referencedFile) {
740✔
1453
                //skip the default bslib file, it will exist at transpile time but should not show up in the program during validation cycle
1454
                if (scriptImport.destPath === this.event.program.bslibPkgPath) {
22✔
1455
                    continue;
2✔
1456
                }
1457
                let dInfo: DiagnosticInfo;
1458
                if (scriptImport.text.trim().length === 0) {
20✔
1459
                    dInfo = DiagnosticMessages.scriptSrcCannotBeEmpty();
1✔
1460
                } else {
1461
                    dInfo = DiagnosticMessages.referencedFileDoesNotExist();
19✔
1462
                }
1463

1464
                this.addMultiScopeDiagnostic({
20✔
1465
                    ...dInfo,
1466
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
1467
                }, ScopeValidatorDiagnosticTag.Imports);
1468
                //if the character casing of the script import path does not match that of the actual path
1469
            } else if (scriptImport.destPath !== referencedFile.destPath) {
718✔
1470
                //preserve the original import shape (pkg:/... vs relative) when reporting the
1471
                //correct path so the quick-fix replacement matches what the user authored.
1472
                const correctUri = scriptImport.text.startsWith('pkg:/')
9✔
1473
                    ? util.sanitizePkgPath(referencedFile.destPath)
9✔
1474
                    : path.posix.relative(
1475
                        path.dirname(scriptImport.sourceFile.destPath).replace(/\\/g, '/'),
1476
                        referencedFile.destPath.replace(/\\/g, '/')
1477
                    );
1478
                this.addMultiScopeDiagnostic({
9✔
1479
                    ...DiagnosticMessages.scriptImportCaseMismatch(referencedFile.destPath, correctUri),
1480
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
1481
                }, ScopeValidatorDiagnosticTag.Imports);
1482
            }
1483
        }
1484
    }
1485

1486
    /**
1487
     * Validate all classes defined in this scope
1488
     */
1489
    private validateClasses() {
1490
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Classes });
2,404✔
1491

1492
        let validator = new BsClassValidator(this.event.scope);
2,404✔
1493
        validator.validate();
2,404✔
1494
        for (const diagnostic of validator.diagnostics) {
2,404✔
1495
            this.addMultiScopeDiagnostic({
33✔
1496
                ...diagnostic
1497
            }, ScopeValidatorDiagnosticTag.Classes);
1498
        }
1499
    }
1500

1501

1502
    /**
1503
     * Find various function collisions
1504
     */
1505
    private diagnosticDetectFunctionCollisions(file: BrsFile) {
1506
        const fileUri = util.pathToUri(file.srcPath);
2,855✔
1507
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, tag: ScopeValidatorDiagnosticTag.FunctionCollisions });
2,855✔
1508
        for (let func of file.callables) {
2,855✔
1509
            const funcName = func.getName(ParseMode.BrighterScript);
2,603✔
1510
            const lowerFuncName = funcName?.toLowerCase();
2,603!
1511
            if (lowerFuncName) {
2,603!
1512

1513
                //find function declarations with the same name as a stdlib function
1514
                if (globalCallableMap.has(lowerFuncName)) {
2,603✔
1515
                    this.addMultiScopeDiagnostic({
5✔
1516
                        ...DiagnosticMessages.scopeFunctionShadowedByBuiltInFunction(),
1517
                        location: util.createLocationFromRange(fileUri, func.nameRange)
1518

1519
                    });
1520
                }
1521
            }
1522
        }
1523
    }
1524

1525
    public detectShadowedLocalVar(file: BrsFile, varDeclaration: { expr: AstNode; name: string; type: BscType; nameRange: Range }) {
1526
        const varName = varDeclaration.name;
2,442✔
1527
        const lowerVarName = varName.toLowerCase();
2,442✔
1528
        const callableContainerMap = this.event.scope.getCallableContainerMap();
2,442✔
1529
        const containingNamespace = varDeclaration.expr?.findAncestor<NamespaceStatement>(isNamespaceStatement);
2,442!
1530
        const localVarIsInNamespace = util.isVariableMemberOfNamespace(varDeclaration.name, varDeclaration.expr, containingNamespace);
2,442✔
1531

1532
        const varIsFunction = () => {
2,442✔
1533
            return isCallableType(varDeclaration.type) && !isDynamicType(varDeclaration.type);
11✔
1534
        };
1535

1536
        if (
2,442✔
1537
            //has same name as stdlib
1538
            globalCallableMap.has(lowerVarName)
1539
        ) {
1540
            //local var function with same name as stdlib function
1541
            if (varIsFunction()) {
8✔
1542
                this.addMultiScopeDiagnostic({
1✔
1543
                    ...DiagnosticMessages.localVarFunctionShadowsParentFunction('stdlib'),
1544
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange)
1545
                });
1546
            }
1547
        } else if (callableContainerMap.has(lowerVarName) && !localVarIsInNamespace) {
2,434✔
1548
            const callable = callableContainerMap.get(lowerVarName);
3✔
1549
            //is same name as a callable
1550
            if (varIsFunction()) {
3✔
1551
                this.addMultiScopeDiagnostic({
1✔
1552
                    ...DiagnosticMessages.localVarFunctionShadowsParentFunction('scope'),
1553
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1554
                    relatedInformation: [{
1555
                        message: 'Function declared here',
1556
                        location: util.createLocationFromFileRange(
1557
                            callable[0].callable.file,
1558
                            callable[0].callable.nameRange
1559
                        )
1560
                    }]
1561
                });
1562
            } else {
1563
                this.addMultiScopeDiagnostic({
2✔
1564
                    ...DiagnosticMessages.localVarShadowedByScopedFunction(),
1565
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1566
                    relatedInformation: [{
1567
                        message: 'Function declared here',
1568
                        location: util.createLocationFromRange(
1569
                            util.pathToUri(callable[0].callable.file.srcPath),
1570
                            callable[0].callable.nameRange
1571
                        )
1572
                    }]
1573
                });
1574
            }
1575
            //has the same name as an in-scope class
1576
        } else if (!localVarIsInNamespace) {
2,431✔
1577
            const classStmtLink = this.event.scope.getClassFileLink(lowerVarName);
2,430✔
1578
            if (classStmtLink) {
2,430✔
1579
                this.addMultiScopeDiagnostic({
4✔
1580
                    ...DiagnosticMessages.localVarShadowedByScopedFunction(),
1581
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1582
                    relatedInformation: [{
1583
                        message: 'Class declared here',
1584
                        location: util.createLocationFromRange(
1585
                            util.pathToUri(classStmtLink.file.srcPath),
1586
                            classStmtLink?.item.tokens.name.location?.range
24!
1587
                        )
1588
                    }]
1589
                });
1590
            }
1591
        }
1592
    }
1593

1594
    private validateForStatement(file: BrsFile, forStmt: ForStatement) {
1595
        const assignStmt = forStmt.counterDeclaration;
22✔
1596
        const assignValueType = this.getNodeTypeWrapper(file, assignStmt.value, { flags: SymbolTypeFlag.runtime, statementIndex: forStmt.statementIndex });
22✔
1597
        if (!IntegerType.instance.isTypeCompatible(assignValueType)) {
22✔
1598
            this.addMultiScopeDiagnostic({
1✔
1599
                ...DiagnosticMessages.assignmentTypeMismatch(assignValueType.toString(), 'integer'),
1600
                location: assignStmt.location
1601
            });
1602
        }
1603
        if (forStmt.increment) {
22✔
1604
            const incrementValueType = this.getNodeTypeWrapper(file, forStmt.increment, { flags: SymbolTypeFlag.runtime, statementIndex: forStmt.statementIndex });
7✔
1605
            if (!IntegerType.instance.isTypeCompatible(incrementValueType)) {
7✔
1606
                this.addMultiScopeDiagnostic({
1✔
1607
                    ...DiagnosticMessages.assignmentTypeMismatch(incrementValueType.toString(), 'integer'),
1608
                    location: forStmt.increment.location
1609
                });
1610
            }
1611
        }
1612
        const finalValueType = this.getNodeTypeWrapper(file, forStmt.finalValue, { flags: SymbolTypeFlag.runtime, statementIndex: forStmt.statementIndex });
22✔
1613
        if (!IntegerType.instance.isTypeCompatible(finalValueType)) {
22✔
1614
            this.addMultiScopeDiagnostic({
1✔
1615
                ...DiagnosticMessages.assignmentTypeMismatch(finalValueType.toString(), 'integer'),
1616
                location: forStmt.finalValue.location
1617
            });
1618
        }
1619
    }
1620

1621
    private validateForEachStatement(file: BrsFile, forEachStmt: ForEachStatement) {
1622
        const targetType = this.getNodeTypeWrapper(file, forEachStmt.target, { flags: SymbolTypeFlag.runtime, statementIndex: forEachStmt.statementIndex });
59✔
1623
        if (isDynamicType(targetType) || isObjectType(targetType)) {
59✔
1624
            // unable to determine type, skip further validation
1625
            return;
10✔
1626
        }
1627

1628
        let targetItemType: BscType;
1629

1630
        if (!isArrayType(targetType)) {
49✔
1631
            if (isIterableType(targetType)) {
18✔
1632
                // this is enumerable
1633
                targetItemType = util.getIteratorDefaultType(targetType);
16✔
1634
            } else {
1635
                // target is not an array nor enumerable
1636
                this.addMultiScopeDiagnostic({
2✔
1637
                    ...DiagnosticMessages.notIterable(targetType.toString()),
1638
                    location: forEachStmt.target.location
1639
                });
1640
                return;
2✔
1641
            }
1642
        } else {
1643
            targetItemType = targetType.defaultType;
31✔
1644
        }
1645

1646
        const loopType = forEachStmt.getLoopVariableType({ flags: SymbolTypeFlag.runtime, statementIndex: forEachStmt.statementIndex });
47✔
1647
        if (forEachStmt.typeExpression && loopType?.isResolvable()) {
47!
1648

1649
            const data: TypeCompatibilityData = {};
6✔
1650
            if (!loopType.isTypeCompatible(targetItemType, data)) {
6✔
1651
                this.addMultiScopeDiagnostic({
4✔
1652
                    ...DiagnosticMessages.assignmentTypeMismatch(targetItemType.toString(), loopType.toString(), data),
1653
                    location: forEachStmt.typeExpression.location
1654
                });
1655
            }
1656
        }
1657
    }
1658

1659
    private validateXmlInterface(scope: XmlScope) {
1660
        if (!scope.xmlFile.parser.ast?.componentElement?.interfaceElement) {
627!
1661
            return;
530✔
1662
        }
1663
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLInterface });
97!
1664

1665
        const iface = scope.xmlFile.parser.ast.componentElement.interfaceElement;
97✔
1666
        const callableContainerMap = scope.getCallableContainerMap();
97✔
1667
        //validate functions
1668
        for (const func of iface.functions) {
97✔
1669
            const name = func.name;
85✔
1670
            if (!name) {
85✔
1671
                this.addDiagnostic({
3✔
1672
                    ...DiagnosticMessages.xmlTagMissingAttribute(func.tokens.startTagName.text, 'name'),
1673
                    location: func.tokens.startTagName.location
1674
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1675
            } else if (!callableContainerMap.has(name.toLowerCase())) {
82✔
1676
                this.addDiagnostic({
4✔
1677
                    ...DiagnosticMessages.xmlFunctionNotFound(name),
1678
                    location: func.getAttribute('name')?.tokens.value.location
12!
1679
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1680
            }
1681
        }
1682
        //validate fields
1683
        for (const field of iface.fields) {
97✔
1684
            const { id, type, onChange } = field;
48✔
1685
            if (!id) {
48✔
1686
                this.addDiagnostic({
3✔
1687
                    ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'id'),
1688
                    location: field.tokens.startTagName.location
1689
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1690
            }
1691
            if (!type) {
48✔
1692
                if (!field.alias) {
3✔
1693
                    this.addDiagnostic({
2✔
1694
                        ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'type'),
1695
                        location: field.tokens.startTagName.location
1696
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1697
                }
1698
            } else if (!SGFieldTypes.includes(type.toLowerCase())) {
45✔
1699
                this.addDiagnostic({
1✔
1700
                    ...DiagnosticMessages.xmlInvalidFieldType(type),
1701
                    location: field.getAttribute('type')?.tokens.value.location
3!
1702
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1703
            }
1704
            if (onChange) {
48✔
1705
                if (!callableContainerMap.has(onChange.toLowerCase())) {
1!
1706
                    this.addDiagnostic({
1✔
1707
                        ...DiagnosticMessages.xmlFunctionNotFound(onChange),
1708
                        location: field.getAttribute('onchange')?.tokens.value.location
3!
1709
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1710
                }
1711
            }
1712
        }
1713
    }
1714

1715
    private validateDocComments(node: AstNode) {
1716
        const doc = brsDocParser.parseNode(node);
295✔
1717
        for (const docTag of doc.tags) {
295✔
1718
            const docTypeTag = docTag as BrsDocWithType;
33✔
1719
            if (!docTypeTag.typeExpression || !docTypeTag.location) {
33✔
1720
                continue;
3✔
1721
            }
1722
            const foundType = docTypeTag.typeExpression?.getType({ flags: SymbolTypeFlag.typetime });
30!
1723
            if (!foundType?.isResolvable()) {
30!
1724
                this.addMultiScopeDiagnostic({
8✔
1725
                    ...DiagnosticMessages.cannotFindName(docTypeTag.typeString),
1726
                    location: brsDocParser.getTypeLocationFromToken(docTypeTag.token) ?? docTypeTag.location
24!
1727
                });
1728
            }
1729
        }
1730
    }
1731

1732
    /**
1733
     * Detect when a child has imported a script that an ancestor also imported
1734
     */
1735
    private diagnosticDetectDuplicateAncestorScriptImports(scope: XmlScope) {
1736
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLImports });
627!
1737
        if (scope.xmlFile.parentComponent) {
627✔
1738
            //build a lookup of pkg paths -> FileReference so we can more easily look up collisions
1739
            let parentScriptImports = scope.xmlFile.getAncestorScriptTagImports();
36✔
1740
            let lookup = {} as Record<string, FileReference>;
36✔
1741
            for (let parentScriptImport of parentScriptImports) {
36✔
1742
                //keep the first occurance of a pkgPath. Parent imports are first in the array
1743
                if (!lookup[parentScriptImport.destPath]) {
33!
1744
                    lookup[parentScriptImport.destPath] = parentScriptImport;
33✔
1745
                }
1746
            }
1747

1748
            //add warning for every script tag that this file shares with an ancestor
1749
            for (let scriptImport of scope.xmlFile.scriptTagImports) {
36✔
1750
                let ancestorScriptImport = lookup[scriptImport.destPath];
33✔
1751
                if (ancestorScriptImport) {
33✔
1752
                    let ancestorComponent = ancestorScriptImport.sourceFile as XmlFile;
24✔
1753
                    let ancestorComponentName = ancestorComponent.componentName?.text ?? ancestorComponent.destPath;
24!
1754
                    this.addDiagnostic({
24✔
1755
                        location: util.createLocationFromFileRange(scope.xmlFile, scriptImport.filePathRange),
1756
                        ...DiagnosticMessages.unnecessaryScriptImportInChildFromParent(ancestorComponentName)
1757
                    }, ScopeValidatorDiagnosticTag.XMLImports);
1758
                }
1759
            }
1760
        }
1761
    }
1762

1763
    /**
1764
     * Wraps the AstNode.getType() method, so that we can do extra processing on the result based on the current file
1765
     * In particular, since BrightScript does not support Unions, and there's no way to cast them to something else
1766
     * if the result of .getType() is a union, and we're in a .brs (brightScript) file, treat the result as Dynamic
1767
     *
1768
     * Also, for BrightScript parse-mode, if .getType() returns a node type, do not validate unknown members.
1769
     *
1770
     * In most cases, this returns the result of node.getType()
1771
     *
1772
     * @param file the current file being processed
1773
     * @param node the node to get the type of
1774
     * @param getTypeOpts any options to pass to node.getType()
1775
     * @returns the processed result type
1776
     */
1777
    private getNodeTypeWrapper(file: BrsFile, node: AstNode, getTypeOpts: GetTypeOptions) {
1778
        const type = node?.getType(getTypeOpts);
11,925!
1779

1780
        if (file.parseMode === ParseMode.BrightScript) {
11,925✔
1781
            // this is a brightscript file
1782
            const typeChain = getTypeOpts.typeChain;
1,248✔
1783
            if (typeChain) {
1,248✔
1784
                const hasUnion = typeChain.reduce((hasUnion, tce) => {
428✔
1785
                    return hasUnion || isUnionType(tce.type);
504✔
1786
                }, false);
1787
                if (hasUnion) {
428✔
1788
                    // there was a union somewhere in the typechain
1789
                    return DynamicType.instance;
1✔
1790
                }
1791
            }
1792
            if (isUnionType(type)) {
1,247!
1793
                //this is a union
UNCOV
1794
                return DynamicType.instance;
×
1795
            }
1796

1797
            if (isComponentType(type)) {
1,247✔
1798
                // modify type to allow any member access for Node types
1799
                type.changeUnknownMemberToDynamic = true;
18✔
1800
            }
1801
        }
1802

1803
        // by default return the result of node.getType()
1804
        return type;
11,924✔
1805
    }
1806

1807
    private getParentTypeDescriptor(typeChainResult: TypeChainProcessResult) {
1808
        if (typeChainResult.itemParentTypeKind === BscTypeKind.NamespaceType) {
289✔
1809
            return 'namespace';
121✔
1810
        }
1811
        return 'type';
168✔
1812
    }
1813

1814
    private addDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1815
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
54!
1816
        this.event.program.diagnostics.register(diagnostic, {
54✔
1817
            tags: [diagnosticTag],
1818
            segment: this.currentSegmentBeingValidated
1819
        });
1820
    }
1821

1822
    /**
1823
     * Add a diagnostic (to the first scope) that will have `relatedInformation` for each affected scope
1824
     */
1825
    private addMultiScopeDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1826
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
696✔
1827
        this.event.program.diagnostics.register(diagnostic, {
696✔
1828
            tags: [diagnosticTag],
1829
            segment: this.currentSegmentBeingValidated,
1830
            scope: this.event.scope
1831
        });
1832
    }
1833
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc