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

rokucommunity / brighterscript / #14387

09 May 2025 11:44AM UTC coverage: 87.032% (-2.0%) from 89.017%
#14387

push

web-flow
Merge a194c3925 into 489231ac7

13732 of 16677 branches covered (82.34%)

Branch coverage included in aggregate %.

8175 of 8874 new or added lines in 103 files covered. (92.12%)

84 existing lines in 22 files now uncovered.

14604 of 15881 relevant lines covered (91.96%)

20324.5 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

294
                let segmentsValidated = 0;
1,846✔
295

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

301

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

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

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

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

354
    private currentSegmentBeingValidated: AstNode;
355

356

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

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

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

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

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

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

446
    }
447

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

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

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

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

499

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

510
    }
511

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

526
        const funcType = util.getCallFuncType(expression, methodToken, { flags: SymbolTypeFlag.runtime, ignoreCall: true });
34✔
527
        if (!funcType?.isResolvable()) {
31✔
528
            this.addMultiScopeDiagnostic({
10✔
529
                ...DiagnosticMessages.cannotFindCallFuncFunction(methodName, functionFullname, callerType.toString()),
530
                location: callErrorLocation
531
            });
532
        }
533

534
        return this.validateFunctionCall(file, expression, funcType, callErrorLocation, expression.args);
31✔
535
    }
536

537
    /**
538
     * Detect calls to functions with the incorrect number of parameters, or wrong types of arguments
539
     */
540
    private validateFunctionCall(file: BrsFile, callee: Expression, funcType: BscType, callErrorLocation: Location, args: Expression[], argOffset = 0) {
1,042✔
541
        if (!funcType?.isResolvable() || !isCallableType(funcType)) {
1,046✔
542
            const funcName = util.getAllDottedGetPartsAsString(callee, ParseMode.BrighterScript, isCallfuncExpression(callee) ? '@.' : '.');
167✔
543
            if (isUnionType(funcType)) {
167✔
544
                if (!util.isUnionOfFunctions(funcType)) {
6!
545
                    // union of func and non func. not callable
NEW
546
                    this.addMultiScopeDiagnostic({
×
547
                        ...DiagnosticMessages.notCallable(funcName),
548
                        location: callErrorLocation
549
                    });
NEW
550
                    return;
×
551
                }
552
                const callablesInUnion = funcType.types.filter(isCallableType);
6✔
553
                const funcsInUnion = callablesInUnion.filter(isTypedFunctionType);
6✔
554
                if (funcsInUnion.length < callablesInUnion.length) {
6!
555
                    // potentially a non-typed func in union
556
                    // cannot validate
NEW
557
                    return;
×
558
                }
559
                // check all funcs to see if they work
560
                for (let i = 1; i < funcsInUnion.length; i++) {
6✔
561
                    const compatibilityData: TypeCompatibilityData = {};
6✔
562
                    if (!funcsInUnion[0].isTypeCompatible(funcsInUnion[i], compatibilityData)) {
6✔
563
                        if (!compatibilityData.returnTypeMismatch) {
4✔
564
                            // param differences!
565
                            this.addMultiScopeDiagnostic({
2✔
566
                                ...DiagnosticMessages.incompatibleSymbolDefinition(
567
                                    funcName,
568
                                    { isUnion: true, data: compatibilityData }),
569
                                location: callErrorLocation
570
                            });
571
                            return;
2✔
572
                        }
573
                    }
574
                }
575
                // The only thing different was return type
576
                funcType = util.getFunctionTypeFromUnion(funcType);
4✔
577

578
            }
579
            if (funcType && !isCallableType(funcType) && !isReferenceType(funcType)) {
165✔
580
                const globalFuncWithVarName = globalCallableMap.get(funcName.toLowerCase());
6✔
581
                if (globalFuncWithVarName) {
6✔
582
                    funcType = globalFuncWithVarName.type;
1✔
583
                } else {
584
                    this.addMultiScopeDiagnostic({
5✔
585
                        ...DiagnosticMessages.notCallable(funcName),
586
                        location: callErrorLocation
587
                    });
588
                    return;
5✔
589
                }
590

591
            }
592
        }
593

594
        if (!isTypedFunctionType(funcType)) {
1,039✔
595
            // non typed function. nothing to check
596
            return;
287✔
597
        }
598

599
        //get min/max parameter count for callable
600
        let minParams = 0;
752✔
601
        let maxParams = 0;
752✔
602
        for (let param of funcType.params) {
752✔
603
            maxParams++;
1,029✔
604
            //optional parameters must come last, so we can assume that minParams won't increase once we hit
605
            //the first isOptional
606
            if (param.isOptional !== true) {
1,029✔
607
                minParams++;
556✔
608
            }
609
        }
610
        if (funcType.isVariadic) {
752✔
611
            // function accepts variable number of arguments
612
            maxParams = CallExpression.MaximumArguments;
12✔
613
        }
614
        const argsForCall = argOffset < 1 ? args : args.slice(argOffset);
752✔
615

616
        let expCallArgCount = argsForCall.length;
752✔
617
        if (expCallArgCount > maxParams || expCallArgCount < minParams) {
752✔
618
            let minMaxParamsText = minParams === maxParams ? maxParams + argOffset : `${minParams + argOffset}-${maxParams + argOffset}`;
33✔
619
            this.addMultiScopeDiagnostic({
33✔
620
                ...DiagnosticMessages.mismatchArgumentCount(minMaxParamsText, expCallArgCount + argOffset),
621
                location: callErrorLocation
622
            });
623
        }
624
        let paramIndex = 0;
752✔
625
        for (let arg of argsForCall) {
752✔
626
            const data = {} as ExtraSymbolData;
647✔
627
            let argType = this.getNodeTypeWrapper(file, arg, { flags: SymbolTypeFlag.runtime, data: data });
647✔
628

629
            const paramType = funcType.params[paramIndex]?.type;
647✔
630
            if (!paramType) {
647✔
631
                // unable to find a paramType -- maybe there are more args than params
632
                break;
22✔
633
            }
634

635
            if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) {
625✔
636
                argType = data.definingNode.getConstructorType();
2✔
637
            }
638

639
            const compatibilityData: TypeCompatibilityData = {};
625✔
640
            const isAllowedArgConversion = this.checkAllowedArgConversions(paramType, argType);
625✔
641
            if (!isAllowedArgConversion && !paramType?.isTypeCompatible(argType, compatibilityData)) {
625!
642
                this.addMultiScopeDiagnostic({
43✔
643
                    ...DiagnosticMessages.argumentTypeMismatch(argType?.toString() ?? 'unknown', paramType?.toString() ?? 'unknown', compatibilityData),
516!
644
                    location: arg.location
645
                });
646
            }
647
            paramIndex++;
625✔
648
        }
649
    }
650

651
    private checkAllowedArgConversions(paramType: BscType, argType: BscType): boolean {
652
        if (isNumberType(argType) && isBooleanType(paramType)) {
625✔
653
            return true;
8✔
654
        }
655
        return false;
617✔
656
    }
657

658

659
    /**
660
     * Detect return statements with incompatible types vs. declared return type
661
     */
662
    private validateReturnStatement(file: BrsFile, returnStmt: ReturnStatement) {
663
        const data: ExtraSymbolData = {};
399✔
664
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: data };
399✔
665
        let funcType = returnStmt.findAncestor(isFunctionExpression)?.getType({ flags: SymbolTypeFlag.typetime });
399✔
666
        if (isTypedFunctionType(funcType)) {
399✔
667
            let actualReturnType = returnStmt?.value
398!
668
                ? this.getNodeTypeWrapper(file, returnStmt?.value, getTypeOptions)
1,535!
669
                : VoidType.instance;
670
            const compatibilityData: TypeCompatibilityData = {};
398✔
671

672
            // `return` statement by itself in non-built-in function will actually result in `invalid`
673
            const valueReturnType = isVoidType(actualReturnType) ? InvalidType.instance : actualReturnType;
398✔
674

675
            if (funcType.returnType.isResolvable()) {
398✔
676
                if (!returnStmt?.value && isVoidType(funcType.returnType)) {
394!
677
                    // allow empty return when function is return `as void`
678
                    // eslint-disable-next-line no-useless-return
679
                    return;
8✔
680
                } else if (!funcType.returnType.isTypeCompatible(valueReturnType, compatibilityData)) {
386✔
681
                    this.addMultiScopeDiagnostic({
28✔
682
                        ...DiagnosticMessages.returnTypeMismatch(actualReturnType.toString(), funcType.returnType.toString(), compatibilityData),
683
                        location: returnStmt.value?.location ?? returnStmt.location
168✔
684
                    });
685
                }
686
            }
687
        }
688
    }
689

690
    /**
691
     * Detect assigned type different from expected member type
692
     */
693
    private validateDottedSetStatement(file: BrsFile, dottedSetStmt: DottedSetStatement) {
694
        const typeChainExpectedLHS = [] as TypeChainEntry[];
101✔
695
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
101✔
696

697
        const expectedLHSType = this.getNodeTypeWrapper(file, dottedSetStmt, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
101✔
698
        const actualRHSType = this.getNodeTypeWrapper(file, dottedSetStmt?.value, getTypeOpts);
101!
699
        const compatibilityData: TypeCompatibilityData = {};
101✔
700
        const typeChainScan = util.processTypeChain(typeChainExpectedLHS);
101✔
701
        // check if anything in typeChain is an AA - if so, just allow it
702
        if (typeChainExpectedLHS.find(typeChainItem => isAssociativeArrayType(typeChainItem.type))) {
177✔
703
            // something in the chain is an AA
704
            // treat members as dynamic - they could have been set without the type system's knowledge
705
            return;
39✔
706
        }
707
        if (!expectedLHSType || !expectedLHSType?.isResolvable()) {
62!
708
            this.addMultiScopeDiagnostic({
5✔
709
                ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
710
                location: typeChainScan?.location
15!
711
            });
712
            return;
5✔
713
        }
714

715
        let accessibilityIsOk = this.checkMemberAccessibility(file, dottedSetStmt, typeChainExpectedLHS);
57✔
716

717
        //Most Component fields can be set with strings
718
        //TODO: be more precise about which fields can actually accept strings
719
        //TODO: if RHS is a string literal, we can do more validation to make sure it's the correct type
720
        if (isComponentType(dottedSetStmt.obj?.getType({ flags: SymbolTypeFlag.runtime }))) {
57!
721
            if (isStringTypeLike(actualRHSType)) {
21✔
722
                return;
6✔
723
            }
724
        }
725

726
        if (accessibilityIsOk && !expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
51!
727
            this.addMultiScopeDiagnostic({
12✔
728
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType?.toString() ?? 'unknown', expectedLHSType?.toString() ?? 'unknown', compatibilityData),
144!
729
                location: dottedSetStmt.location
730
            });
731
        }
732
    }
733

734
    /**
735
     * Detect when declared type does not match rhs type
736
     */
737
    private validateAssignmentStatement(file: BrsFile, assignStmt: AssignmentStatement) {
738
        if (!assignStmt?.typeExpression) {
712!
739
            // nothing to check
740
            return;
704✔
741
        }
742

743
        const typeChainExpectedLHS = [];
8✔
744
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
8✔
745
        const expectedLHSType = this.getNodeTypeWrapper(file, assignStmt.typeExpression, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
8✔
746
        const actualRHSType = this.getNodeTypeWrapper(file, assignStmt.value, getTypeOpts);
8✔
747
        const compatibilityData: TypeCompatibilityData = {};
8✔
748
        if (!expectedLHSType || !expectedLHSType.isResolvable()) {
8✔
749
            // LHS is not resolvable... handled elsewhere
750
        } else if (!expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
7!
751
            this.addMultiScopeDiagnostic({
1✔
752
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
753
                location: assignStmt.location
754
            });
755
        }
756
    }
757

758
    /**
759
     * Detect invalid use of a binary operator
760
     */
761
    private validateBinaryExpression(file: BrsFile, binaryExpr: BinaryExpression | AugmentedAssignmentStatement) {
762
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
355✔
763

764
        if (util.isInTypeExpression(binaryExpr)) {
355✔
765
            return;
20✔
766
        }
767

768
        let leftType = isBinaryExpression(binaryExpr)
335✔
769
            ? this.getNodeTypeWrapper(file, binaryExpr.left, getTypeOpts)
335✔
770
            : this.getNodeTypeWrapper(file, binaryExpr.item, getTypeOpts);
771
        let rightType = isBinaryExpression(binaryExpr)
335✔
772
            ? this.getNodeTypeWrapper(file, binaryExpr.right, getTypeOpts)
335✔
773
            : this.getNodeTypeWrapper(file, binaryExpr.value, getTypeOpts);
774

775
        if (!leftType || !rightType || !leftType.isResolvable() || !rightType.isResolvable()) {
335✔
776
            // Can not find the type. error handled elsewhere
777
            return;
13✔
778
        }
779

780
        let leftTypeToTest = leftType;
322✔
781
        let rightTypeToTest = rightType;
322✔
782

783
        if (isEnumMemberType(leftType) || isEnumType(leftType)) {
322✔
784
            leftTypeToTest = leftType.underlyingType;
11✔
785
        }
786
        if (isEnumMemberType(rightType) || isEnumType(rightType)) {
322✔
787
            rightTypeToTest = rightType.underlyingType;
10✔
788
        }
789

790
        if (isUnionType(leftType) || isUnionType(rightType)) {
322✔
791
            // TODO: it is possible to validate based on innerTypes, but more complicated
792
            // Because you need to verify each combination of types
793
            return;
28✔
794
        }
795
        const opResult = util.binaryOperatorResultType(leftTypeToTest, binaryExpr.tokens.operator, rightTypeToTest);
294✔
796

797
        if (!opResult) {
294✔
798
            // if the result was dynamic or void, that means there wasn't a valid operation
799
            this.addMultiScopeDiagnostic({
9✔
800
                ...DiagnosticMessages.operatorTypeMismatch(binaryExpr.tokens.operator.text, leftType.toString(), rightType.toString()),
801
                location: binaryExpr.location
802
            });
803
        }
804
    }
805

806
    /**
807
     * Detect invalid use of a Unary operator
808
     */
809
    private validateUnaryExpression(file: BrsFile, unaryExpr: UnaryExpression) {
810
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
33✔
811

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

814
        if (!rightType.isResolvable()) {
33!
815
            // Can not find the type. error handled elsewhere
NEW
816
            return;
×
817
        }
818
        let rightTypeToTest = rightType;
33✔
819
        if (isEnumMemberType(rightType)) {
33!
NEW
820
            rightTypeToTest = rightType.underlyingType;
×
821
        }
822

823
        if (isUnionType(rightTypeToTest)) {
33✔
824
            // TODO: it is possible to validate based on innerTypes, but more complicated
825
            // Because you need to verify each combination of types
826

827
        } else if (isDynamicType(rightTypeToTest) || isObjectType(rightTypeToTest)) {
32✔
828
            // operand is basically "any" type... ignore;
829

830
        } else if (isPrimitiveType(rightType)) {
29!
831
            const opResult = util.unaryOperatorResultType(unaryExpr.tokens.operator, rightTypeToTest);
29✔
832
            if (!opResult) {
29✔
833
                this.addMultiScopeDiagnostic({
1✔
834
                    ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
835
                    location: unaryExpr.location
836
                });
837
            }
838
        } else {
839
            // rhs is not a primitive, so no binary operator is allowed
NEW
840
            this.addMultiScopeDiagnostic({
×
841
                ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
842
                location: unaryExpr.location
843
            });
844
        }
845
    }
846

847
    private validateIncrementStatement(file: BrsFile, incStmt: IncrementStatement) {
848
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
11✔
849

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

852
        if (!rightType.isResolvable()) {
11!
853
            // Can not find the type. error handled elsewhere
NEW
854
            return;
×
855
        }
856

857
        if (isUnionType(rightType)) {
11✔
858
            // TODO: it is possible to validate based on innerTypes, but more complicated
859
            // because you need to verify each combination of types
860
        } else if (isDynamicType(rightType) || isObjectType(rightType)) {
9✔
861
            // operand is basically "any" type... ignore
862
        } else if (isNumberType(rightType)) {
8✔
863
            // operand is a number.. this is ok
864
        } else {
865
            // rhs is not a number, so no increment operator is not allowed
866
            this.addMultiScopeDiagnostic({
1✔
867
                ...DiagnosticMessages.operatorTypeMismatch(incStmt.tokens.operator.text, rightType.toString()),
868
                location: incStmt.location
869
            });
870
        }
871
    }
872

873

874
    validateVariableAndDottedGetExpressions(file: BrsFile, expression: VariableExpression | DottedGetExpression) {
875
        if (isDottedGetExpression(expression.parent)) {
5,694✔
876
            // We validate dottedGetExpressions at the top-most level
877
            return;
1,470✔
878
        }
879
        if (isVariableExpression(expression)) {
4,224✔
880
            if (isAssignmentStatement(expression.parent) && expression.parent.tokens.name === expression.tokens.name) {
3,077!
881
                // Don't validate LHS of assignments
NEW
882
                return;
×
883
            } else if (isNamespaceStatement(expression.parent)) {
3,077✔
884
                return;
3✔
885
            }
886
        }
887

888
        let symbolType = SymbolTypeFlag.runtime;
4,221✔
889
        let oppositeSymbolType = SymbolTypeFlag.typetime;
4,221✔
890
        const isUsedAsType = util.isInTypeExpression(expression);
4,221✔
891
        if (isUsedAsType) {
4,221✔
892
            // This is used in a TypeExpression - only look up types from SymbolTable
893
            symbolType = SymbolTypeFlag.typetime;
1,355✔
894
            oppositeSymbolType = SymbolTypeFlag.runtime;
1,355✔
895
        }
896

897
        // Do a complete type check on all DottedGet and Variable expressions
898
        // this will create a diagnostic if an invalid member is accessed
899
        const typeChain: TypeChainEntry[] = [];
4,221✔
900
        const typeData = {} as ExtraSymbolData;
4,221✔
901
        let exprType = this.getNodeTypeWrapper(file, expression, {
4,221✔
902
            flags: symbolType,
903
            typeChain: typeChain,
904
            data: typeData
905
        });
906

907
        const hasValidDeclaration = this.hasValidDeclaration(expression, exprType, typeData?.definingNode);
4,221!
908

909
        //include a hint diagnostic if this type is marked as deprecated
910
        if (typeData.flags & SymbolTypeFlag.deprecated) { // eslint-disable-line no-bitwise
4,221✔
911
            this.addMultiScopeDiagnostic({
2✔
912
                ...DiagnosticMessages.itemIsDeprecated(),
913
                location: expression.tokens.name.location,
914
                tags: [DiagnosticTag.Deprecated]
915
            });
916
        }
917

918
        if (!this.isTypeKnown(exprType) && !hasValidDeclaration) {
4,221✔
919
            if (this.getNodeTypeWrapper(file, expression, { flags: oppositeSymbolType, isExistenceTest: true })?.isResolvable()) {
259!
920
                const oppoSiteTypeChain = [];
5✔
921
                const invalidlyUsedResolvedType = this.getNodeTypeWrapper(file, expression, { flags: oppositeSymbolType, typeChain: oppoSiteTypeChain, isExistenceTest: true });
5✔
922
                const typeChainScan = util.processTypeChain(oppoSiteTypeChain);
5✔
923
                if (isUsedAsType) {
5✔
924
                    this.addMultiScopeDiagnostic({
2✔
925
                        ...DiagnosticMessages.itemCannotBeUsedAsType(typeChainScan.fullChainName),
926
                        location: expression.location
927
                    });
928
                } else if (invalidlyUsedResolvedType && !isReferenceType(invalidlyUsedResolvedType)) {
3✔
929
                    if (!isAliasStatement(expression.parent)) {
1!
930
                        // alias rhs CAN be a type!
NEW
931
                        this.addMultiScopeDiagnostic({
×
932
                            ...DiagnosticMessages.itemCannotBeUsedAsVariable(invalidlyUsedResolvedType.toString()),
933
                            location: expression.location
934
                        });
935
                    }
936
                } else {
937
                    const typeChainScan = util.processTypeChain(typeChain);
2✔
938
                    //if this is a function call, provide a different diagnostic code
939
                    if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
2✔
940
                        this.addMultiScopeDiagnostic({
1✔
941
                            ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
942
                            location: typeChainScan?.location
3!
943
                        });
944
                    } else {
945
                        this.addMultiScopeDiagnostic({
1✔
946
                            ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
947
                            location: typeChainScan?.location
3!
948
                        });
949
                    }
950
                }
951

952
            } else if (!(typeData?.isFromDocComment)) {
254!
953
                // only show "cannot find... " errors if the type is not defined from a doc comment
954
                const typeChainScan = util.processTypeChain(typeChain);
252✔
955
                if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
252✔
956
                    this.addMultiScopeDiagnostic({
27✔
957
                        ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
958
                        location: typeChainScan?.location
81!
959
                    });
960
                } else {
961
                    this.addMultiScopeDiagnostic({
225✔
962
                        ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
963
                        location: typeChainScan?.location
675!
964
                    });
965
                }
966

967
            }
968
        }
969
        if (isUsedAsType) {
4,221✔
970
            return;
1,355✔
971
        }
972

973
        const containingNamespace = expression.findAncestor<NamespaceStatement>(isNamespaceStatement);
2,866✔
974
        const containingNamespaceName = containingNamespace?.getName(ParseMode.BrighterScript);
2,866✔
975

976
        if (!(isCallExpression(expression.parent) && isNewExpression(expression.parent?.parent))) {
2,866!
977
            const classUsedAsVarEntry = this.checkTypeChainForClassUsedAsVar(typeChain, containingNamespaceName);
2,745✔
978
            const isClassInNamespace = containingNamespace?.getSymbolTable().hasSymbol(typeChain[0].name, SymbolTypeFlag.runtime);
2,745✔
979
            if (classUsedAsVarEntry && !isClassInNamespace) {
2,745!
980

NEW
981
                this.addMultiScopeDiagnostic({
×
982
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable(classUsedAsVarEntry.toString()),
983
                    location: expression.location
984
                });
NEW
985
                return;
×
986
            }
987
        }
988

989
        const lastTypeInfo = typeChain[typeChain.length - 1];
2,866✔
990
        const parentTypeInfo = typeChain[typeChain.length - 2];
2,866✔
991

992
        this.checkMemberAccessibility(file, expression, typeChain);
2,866✔
993

994
        if (isNamespaceType(exprType) && !isAliasStatement(expression.parent)) {
2,866✔
995
            this.addMultiScopeDiagnostic({
24✔
996
                ...DiagnosticMessages.itemCannotBeUsedAsVariable('namespace'),
997
                location: expression.location
998
            });
999
        } else if (isEnumType(exprType) && !isAliasStatement(expression.parent)) {
2,842✔
1000
            const enumStatement = this.event.scope.getEnum(util.getAllDottedGetPartsAsString(expression));
27✔
1001
            if (enumStatement) {
27✔
1002
                // there's an enum with this name
1003
                this.addMultiScopeDiagnostic({
4✔
1004
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable('enum'),
1005
                    location: expression.location
1006
                });
1007
            }
1008
        } else if (isDynamicType(exprType) && isEnumType(parentTypeInfo?.type) && isDottedGetExpression(expression)) {
2,815✔
1009
            const enumFileLink = this.event.scope.getEnumFileLink(util.getAllDottedGetPartsAsString(expression.obj));
12✔
1010
            const typeChainScanForItem = util.processTypeChain(typeChain);
12✔
1011
            const typeChainScanForParent = util.processTypeChain(typeChain.slice(0, -1));
12✔
1012
            if (enumFileLink) {
12✔
1013
                this.addMultiScopeDiagnostic({
8✔
1014
                    ...DiagnosticMessages.cannotFindName(lastTypeInfo?.name, typeChainScanForItem.fullChainName, typeChainScanForParent.fullNameOfItem, 'enum'),
24!
1015
                    location: lastTypeInfo?.location,
24!
1016
                    relatedInformation: [{
1017
                        message: 'Enum declared here',
1018
                        location: util.createLocationFromRange(
1019
                            util.pathToUri(enumFileLink?.file.srcPath),
24!
1020
                            enumFileLink?.item?.tokens.name.location?.range
72!
1021
                        )
1022
                    }]
1023
                });
1024
            }
1025
        }
1026
    }
1027

1028
    private checkTypeChainForClassUsedAsVar(typeChain: TypeChainEntry[], containingNamespaceName: string) {
1029
        const ignoreKinds = [AstNodeKind.TypecastExpression, AstNodeKind.NewExpression];
2,745✔
1030
        let lowerNameSoFar = '';
2,745✔
1031
        let classUsedAsVar;
1032
        let isFirst = true;
2,745✔
1033
        for (let i = 0; i < typeChain.length - 1; i++) { // do not look at final entry - we CAN use the constructor as a variable
2,745✔
1034
            const tce = typeChain[i];
1,374✔
1035
            lowerNameSoFar += `${lowerNameSoFar ? '.' : ''}${tce.name.toLowerCase()}`;
1,374✔
1036
            if (!isNamespaceType(tce.type)) {
1,374✔
1037
                if (isFirst && containingNamespaceName) {
689✔
1038
                    lowerNameSoFar = `${containingNamespaceName.toLowerCase()}.${lowerNameSoFar}`;
77✔
1039
                }
1040
                if (!tce.astNode || ignoreKinds.includes(tce.astNode.kind)) {
689✔
1041
                    break;
15✔
1042
                } else if (isClassType(tce.type) && lowerNameSoFar.toLowerCase() === tce.type.name.toLowerCase()) {
674✔
1043
                    classUsedAsVar = tce.type;
1✔
1044
                }
1045
                break;
674✔
1046
            }
1047
            isFirst = false;
685✔
1048
        }
1049

1050
        return classUsedAsVar;
2,745✔
1051
    }
1052

1053
    /**
1054
     * Adds diagnostics for accibility mismatches
1055
     *
1056
     * @param file file
1057
     * @param expression containing expression
1058
     * @param typeChain type chain to check
1059
     * @returns true if member accesiibility is okay
1060
     */
1061
    private checkMemberAccessibility(file: BscFile, expression: Expression, typeChain: TypeChainEntry[]) {
1062
        for (let i = 0; i < typeChain.length - 1; i++) {
2,923✔
1063
            const parentChainItem = typeChain[i];
1,556✔
1064
            const childChainItem = typeChain[i + 1];
1,556✔
1065
            if (isClassType(parentChainItem.type)) {
1,556✔
1066
                const containingClassStmt = expression.findAncestor<ClassStatement>(isClassStatement);
159✔
1067
                const classStmtThatDefinesChildMember = childChainItem.data?.definingNode?.findAncestor<ClassStatement>(isClassStatement);
159!
1068
                if (classStmtThatDefinesChildMember) {
159✔
1069
                    const definingClassName = classStmtThatDefinesChildMember.getName(ParseMode.BrighterScript);
157✔
1070
                    const inMatchingClassStmt = containingClassStmt?.getName(ParseMode.BrighterScript).toLowerCase() === parentChainItem.type.name.toLowerCase();
157✔
1071
                    // eslint-disable-next-line no-bitwise
1072
                    if (childChainItem.data.flags & SymbolTypeFlag.private) {
157✔
1073
                        if (!inMatchingClassStmt || childChainItem.data.memberOfAncestor) {
16✔
1074
                            this.addMultiScopeDiagnostic({
4✔
1075
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
1076
                                location: expression.location
1077
                            });
1078
                            // there's an error... don't worry about the rest of the chain
1079
                            return false;
4✔
1080
                        }
1081
                    }
1082

1083
                    // eslint-disable-next-line no-bitwise
1084
                    if (childChainItem.data.flags & SymbolTypeFlag.protected) {
153✔
1085
                        const containingClassName = containingClassStmt?.getName(ParseMode.BrighterScript);
13✔
1086
                        const containingNamespaceName = expression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
13✔
1087
                        const ancestorClasses = this.event.scope.getClassHierarchy(containingClassName, containingNamespaceName).map(link => link.item);
13✔
1088
                        const isSubClassOfDefiningClass = ancestorClasses.includes(classStmtThatDefinesChildMember);
13✔
1089

1090
                        if (!isSubClassOfDefiningClass) {
13✔
1091
                            this.addMultiScopeDiagnostic({
5✔
1092
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
1093
                                location: expression.location
1094
                            });
1095
                            // there's an error... don't worry about the rest of the chain
1096
                            return false;
5✔
1097
                        }
1098
                    }
1099
                }
1100

1101
            }
1102
        }
1103
        return true;
2,914✔
1104
    }
1105

1106
    /**
1107
     * Find all "new" statements in the program,
1108
     * and make sure we can find a class with that name
1109
     */
1110
    private validateNewExpression(file: BrsFile, newExpression: NewExpression) {
1111
        const newExprType = this.getNodeTypeWrapper(file, newExpression, { flags: SymbolTypeFlag.typetime });
121✔
1112
        if (isClassType(newExprType)) {
121✔
1113
            return;
113✔
1114
        }
1115

1116
        let potentialClassName = newExpression.className.getName(ParseMode.BrighterScript);
8✔
1117
        const namespaceName = newExpression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
8!
1118
        let newableClass = this.event.scope.getClass(potentialClassName, namespaceName);
8✔
1119

1120
        if (!newableClass) {
8!
1121
            //try and find functions with this name.
1122
            let fullName = util.getFullyQualifiedClassName(potentialClassName, namespaceName);
8✔
1123

1124
            this.addMultiScopeDiagnostic({
8✔
1125
                ...DiagnosticMessages.expressionIsNotConstructable(fullName),
1126
                location: newExpression.className.location
1127
            });
1128

1129
        }
1130
    }
1131

1132
    private validateFunctionExpressionForReturn(func: FunctionExpression) {
1133
        const returnType = func?.returnTypeExpression?.getType({ flags: SymbolTypeFlag.typetime });
2,199!
1134

1135
        if (!returnType || !returnType.isResolvable() || isVoidType(returnType) || isDynamicType(returnType)) {
2,199✔
1136
            return;
1,950✔
1137
        }
1138
        const returns = func.body?.findChild<ReturnStatement>(isReturnStatement, { walkMode: WalkMode.visitAll });
249!
1139
        if (!returns && isStringTypeLike(returnType)) {
249✔
1140
            this.addMultiScopeDiagnostic({
5✔
1141
                ...DiagnosticMessages.returnTypeCoercionMismatch(returnType.toString()),
1142
                location: func.location
1143
            });
1144
        }
1145
    }
1146

1147
    /**
1148
     * Create diagnostics for any duplicate function declarations
1149
     */
1150
    private flagDuplicateFunctionDeclarations() {
1151
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration });
1,917✔
1152

1153
        //for each list of callables with the same name
1154
        for (let [lowerName, callableContainers] of this.event.scope.getCallableContainerMap()) {
1,917✔
1155

1156
            let globalCallables = [] as CallableContainer[];
143,938✔
1157
            let nonGlobalCallables = [] as CallableContainer[];
143,938✔
1158
            let ownCallables = [] as CallableContainer[];
143,938✔
1159
            let ancestorNonGlobalCallables = [] as CallableContainer[];
143,938✔
1160

1161

1162
            for (let container of callableContainers) {
143,938✔
1163
                if (container.scope === this.event.program.globalScope) {
149,723✔
1164
                    globalCallables.push(container);
147,609✔
1165
                } else {
1166
                    nonGlobalCallables.push(container);
2,114✔
1167
                    if (container.scope === this.event.scope) {
2,114✔
1168
                        ownCallables.push(container);
2,084✔
1169
                    } else {
1170
                        ancestorNonGlobalCallables.push(container);
30✔
1171
                    }
1172
                }
1173
            }
1174

1175
            //add info diagnostics about child shadowing parent functions
1176
            if (ownCallables.length > 0 && ancestorNonGlobalCallables.length > 0) {
143,938✔
1177
                for (let container of ownCallables) {
24✔
1178
                    //skip the init function (because every component will have one of those){
1179
                    if (lowerName !== 'init') {
24✔
1180
                        let shadowedCallable = ancestorNonGlobalCallables[ancestorNonGlobalCallables.length - 1];
23✔
1181
                        if (!!shadowedCallable && shadowedCallable.callable.file === container.callable.file) {
23✔
1182
                            //same file: skip redundant imports
1183
                            continue;
20✔
1184
                        }
1185
                        this.addMultiScopeDiagnostic({
3✔
1186
                            ...DiagnosticMessages.overridesAncestorFunction(
1187
                                container.callable.name,
1188
                                container.scope.name,
1189
                                shadowedCallable.callable.file.destPath,
1190
                                //grab the last item in the list, which should be the closest ancestor's version
1191
                                shadowedCallable.scope.name
1192
                            ),
1193
                            location: util.createLocationFromFileRange(container.callable.file, container.callable.nameRange)
1194
                        }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
1195
                    }
1196
                }
1197
            }
1198

1199
            //add error diagnostics about duplicate functions in the same scope
1200
            if (ownCallables.length > 1) {
143,938✔
1201

1202
                for (let callableContainer of ownCallables) {
5✔
1203
                    let callable = callableContainer.callable;
10✔
1204
                    const related = [];
10✔
1205
                    for (const ownCallable of ownCallables) {
10✔
1206
                        const thatNameRange = ownCallable.callable.nameRange;
20✔
1207
                        if (ownCallable.callable.nameRange !== callable.nameRange) {
20✔
1208
                            related.push({
10✔
1209
                                message: `Function declared here`,
1210
                                location: util.createLocationFromRange(
1211
                                    util.pathToUri(ownCallable.callable.file?.srcPath),
30!
1212
                                    thatNameRange
1213
                                )
1214
                            });
1215
                        }
1216
                    }
1217

1218
                    this.addMultiScopeDiagnostic({
10✔
1219
                        ...DiagnosticMessages.duplicateFunctionImplementation(callable.name),
1220
                        location: util.createLocationFromFileRange(callable.file, callable.nameRange),
1221
                        relatedInformation: related
1222
                    }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
1223
                }
1224
            }
1225
        }
1226
    }
1227

1228
    /**
1229
     * Verify that all of the scripts imported by each file in this scope actually exist, and have the correct case
1230
     */
1231
    private validateScriptImportPaths() {
1232
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Imports });
1,917✔
1233

1234
        let scriptImports = this.event.scope.getOwnScriptImports();
1,917✔
1235
        //verify every script import
1236
        for (let scriptImport of scriptImports) {
1,917✔
1237
            let referencedFile = this.event.scope.getFileByRelativePath(scriptImport.destPath);
597✔
1238
            //if we can't find the file
1239
            if (!referencedFile) {
597✔
1240
                //skip the default bslib file, it will exist at transpile time but should not show up in the program during validation cycle
1241
                if (scriptImport.destPath === this.event.program.bslibPkgPath) {
17✔
1242
                    continue;
2✔
1243
                }
1244
                let dInfo: DiagnosticInfo;
1245
                if (scriptImport.text.trim().length === 0) {
15✔
1246
                    dInfo = DiagnosticMessages.scriptSrcCannotBeEmpty();
1✔
1247
                } else {
1248
                    dInfo = DiagnosticMessages.referencedFileDoesNotExist();
14✔
1249
                }
1250

1251
                this.addMultiScopeDiagnostic({
15✔
1252
                    ...dInfo,
1253
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
1254
                }, ScopeValidatorDiagnosticTag.Imports);
1255
                //if the character casing of the script import path does not match that of the actual path
1256
            } else if (scriptImport.destPath !== referencedFile.destPath) {
580✔
1257
                this.addMultiScopeDiagnostic({
2✔
1258
                    ...DiagnosticMessages.scriptImportCaseMismatch(referencedFile.destPath),
1259
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
1260
                }, ScopeValidatorDiagnosticTag.Imports);
1261
            }
1262
        }
1263
    }
1264

1265
    /**
1266
     * Validate all classes defined in this scope
1267
     */
1268
    private validateClasses() {
1269
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Classes });
1,917✔
1270

1271
        let validator = new BsClassValidator(this.event.scope);
1,917✔
1272
        validator.validate();
1,917✔
1273
        for (const diagnostic of validator.diagnostics) {
1,917✔
1274
            this.addMultiScopeDiagnostic({
29✔
1275
                ...diagnostic
1276
            }, ScopeValidatorDiagnosticTag.Classes);
1277
        }
1278
    }
1279

1280

1281
    /**
1282
     * Find various function collisions
1283
     */
1284
    private diagnosticDetectFunctionCollisions(file: BrsFile) {
1285
        const fileUri = util.pathToUri(file.srcPath);
2,254✔
1286
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, tag: ScopeValidatorDiagnosticTag.FunctionCollisions });
2,254✔
1287
        for (let func of file.callables) {
2,254✔
1288
            const funcName = func.getName(ParseMode.BrighterScript);
2,043✔
1289
            const lowerFuncName = funcName?.toLowerCase();
2,043!
1290
            if (lowerFuncName) {
2,043!
1291

1292
                //find function declarations with the same name as a stdlib function
1293
                if (globalCallableMap.has(lowerFuncName)) {
2,043✔
1294
                    this.addMultiScopeDiagnostic({
5✔
1295
                        ...DiagnosticMessages.scopeFunctionShadowedByBuiltInFunction(),
1296
                        location: util.createLocationFromRange(fileUri, func.nameRange)
1297

1298
                    });
1299
                }
1300
            }
1301
        }
1302
    }
1303

1304
    public detectShadowedLocalVar(file: BrsFile, varDeclaration: { expr: AstNode; name: string; type: BscType; nameRange: Range }) {
1305
        const varName = varDeclaration.name;
1,918✔
1306
        const lowerVarName = varName.toLowerCase();
1,918✔
1307
        const callableContainerMap = this.event.scope.getCallableContainerMap();
1,918✔
1308
        const containingNamespace = varDeclaration.expr?.findAncestor<NamespaceStatement>(isNamespaceStatement);
1,918!
1309
        const localVarIsInNamespace = util.isVariableMemberOfNamespace(varDeclaration.name, varDeclaration.expr, containingNamespace);
1,918✔
1310

1311
        const varIsFunction = () => {
1,918✔
1312
            return isCallableType(varDeclaration.type) && !isDynamicType(varDeclaration.type);
10✔
1313
        };
1314

1315
        if (
1,918✔
1316
            //has same name as stdlib
1317
            globalCallableMap.has(lowerVarName)
1318
        ) {
1319
            //local var function with same name as stdlib function
1320
            if (varIsFunction()) {
7✔
1321
                this.addMultiScopeDiagnostic({
1✔
1322
                    ...DiagnosticMessages.localVarFunctionShadowsParentFunction('stdlib'),
1323
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange)
1324
                });
1325
            }
1326
        } else if (callableContainerMap.has(lowerVarName) && !localVarIsInNamespace) {
1,911✔
1327
            const callable = callableContainerMap.get(lowerVarName);
3✔
1328
            //is same name as a callable
1329
            if (varIsFunction()) {
3✔
1330
                this.addMultiScopeDiagnostic({
1✔
1331
                    ...DiagnosticMessages.localVarFunctionShadowsParentFunction('scope'),
1332
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1333
                    relatedInformation: [{
1334
                        message: 'Function declared here',
1335
                        location: util.createLocationFromFileRange(
1336
                            callable[0].callable.file,
1337
                            callable[0].callable.nameRange
1338
                        )
1339
                    }]
1340
                });
1341
            } else {
1342
                this.addMultiScopeDiagnostic({
2✔
1343
                    ...DiagnosticMessages.localVarShadowedByScopedFunction(),
1344
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1345
                    relatedInformation: [{
1346
                        message: 'Function declared here',
1347
                        location: util.createLocationFromRange(
1348
                            util.pathToUri(callable[0].callable.file.srcPath),
1349
                            callable[0].callable.nameRange
1350
                        )
1351
                    }]
1352
                });
1353
            }
1354
            //has the same name as an in-scope class
1355
        } else if (!localVarIsInNamespace) {
1,908✔
1356
            const classStmtLink = this.event.scope.getClassFileLink(lowerVarName);
1,907✔
1357
            if (classStmtLink) {
1,907✔
1358
                this.addMultiScopeDiagnostic({
3✔
1359
                    ...DiagnosticMessages.localVarShadowedByScopedFunction(),
1360
                    location: util.createLocationFromFileRange(file, varDeclaration.nameRange),
1361
                    relatedInformation: [{
1362
                        message: 'Class declared here',
1363
                        location: util.createLocationFromRange(
1364
                            util.pathToUri(classStmtLink.file.srcPath),
1365
                            classStmtLink?.item.tokens.name.location?.range
18!
1366
                        )
1367
                    }]
1368
                });
1369
            }
1370
        }
1371
    }
1372

1373
    private validateXmlInterface(scope: XmlScope) {
1374
        if (!scope.xmlFile.parser.ast?.componentElement?.interfaceElement) {
532!
1375
            return;
448✔
1376
        }
1377
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLInterface });
84!
1378

1379
        const iface = scope.xmlFile.parser.ast.componentElement.interfaceElement;
84✔
1380
        const callableContainerMap = scope.getCallableContainerMap();
84✔
1381
        //validate functions
1382
        for (const func of iface.functions) {
84✔
1383
            const name = func.name;
72✔
1384
            if (!name) {
72✔
1385
                this.addDiagnostic({
3✔
1386
                    ...DiagnosticMessages.xmlTagMissingAttribute(func.tokens.startTagName.text, 'name'),
1387
                    location: func.tokens.startTagName.location
1388
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1389
            } else if (!callableContainerMap.has(name.toLowerCase())) {
69✔
1390
                this.addDiagnostic({
4✔
1391
                    ...DiagnosticMessages.xmlFunctionNotFound(name),
1392
                    location: func.getAttribute('name')?.tokens.value.location
12!
1393
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1394
            }
1395
        }
1396
        //validate fields
1397
        for (const field of iface.fields) {
84✔
1398
            const { id, type, onChange } = field;
43✔
1399
            if (!id) {
43✔
1400
                this.addDiagnostic({
3✔
1401
                    ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'id'),
1402
                    location: field.tokens.startTagName.location
1403
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1404
            }
1405
            if (!type) {
43✔
1406
                if (!field.alias) {
3✔
1407
                    this.addDiagnostic({
2✔
1408
                        ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'type'),
1409
                        location: field.tokens.startTagName.location
1410
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1411
                }
1412
            } else if (!SGFieldTypes.includes(type.toLowerCase())) {
40✔
1413
                this.addDiagnostic({
1✔
1414
                    ...DiagnosticMessages.xmlInvalidFieldType(type),
1415
                    location: field.getAttribute('type')?.tokens.value.location
3!
1416
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1417
            }
1418
            if (onChange) {
43✔
1419
                if (!callableContainerMap.has(onChange.toLowerCase())) {
1!
1420
                    this.addDiagnostic({
1✔
1421
                        ...DiagnosticMessages.xmlFunctionNotFound(onChange),
1422
                        location: field.getAttribute('onchange')?.tokens.value.location
3!
1423
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1424
                }
1425
            }
1426
        }
1427
    }
1428

1429
    private validateDocComments(node: AstNode) {
1430
        const doc = brsDocParser.parseNode(node);
251✔
1431
        for (const docTag of doc.tags) {
251✔
1432
            const docTypeTag = docTag as BrsDocWithType;
29✔
1433
            if (!docTypeTag.typeExpression || !docTypeTag.location) {
29✔
1434
                continue;
1✔
1435
            }
1436
            const foundType = docTypeTag.typeExpression?.getType({ flags: SymbolTypeFlag.typetime });
28!
1437
            if (!foundType?.isResolvable()) {
28!
1438
                this.addMultiScopeDiagnostic({
8✔
1439
                    ...DiagnosticMessages.cannotFindName(docTypeTag.typeString),
1440
                    location: brsDocParser.getTypeLocationFromToken(docTypeTag.token) ?? docTypeTag.location
24!
1441
                });
1442
            }
1443
        }
1444
    }
1445

1446
    /**
1447
     * Detect when a child has imported a script that an ancestor also imported
1448
     */
1449
    private diagnosticDetectDuplicateAncestorScriptImports(scope: XmlScope) {
1450
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLImports });
532!
1451
        if (scope.xmlFile.parentComponent) {
532✔
1452
            //build a lookup of pkg paths -> FileReference so we can more easily look up collisions
1453
            let parentScriptImports = scope.xmlFile.getAncestorScriptTagImports();
34✔
1454
            let lookup = {} as Record<string, FileReference>;
34✔
1455
            for (let parentScriptImport of parentScriptImports) {
34✔
1456
                //keep the first occurance of a pkgPath. Parent imports are first in the array
1457
                if (!lookup[parentScriptImport.destPath]) {
30!
1458
                    lookup[parentScriptImport.destPath] = parentScriptImport;
30✔
1459
                }
1460
            }
1461

1462
            //add warning for every script tag that this file shares with an ancestor
1463
            for (let scriptImport of scope.xmlFile.scriptTagImports) {
34✔
1464
                let ancestorScriptImport = lookup[scriptImport.destPath];
30✔
1465
                if (ancestorScriptImport) {
30✔
1466
                    let ancestorComponent = ancestorScriptImport.sourceFile as XmlFile;
21✔
1467
                    let ancestorComponentName = ancestorComponent.componentName?.text ?? ancestorComponent.destPath;
21!
1468
                    this.addDiagnostic({
21✔
1469
                        location: util.createLocationFromFileRange(scope.xmlFile, scriptImport.filePathRange),
1470
                        ...DiagnosticMessages.unnecessaryScriptImportInChildFromParent(ancestorComponentName)
1471
                    }, ScopeValidatorDiagnosticTag.XMLImports);
1472
                }
1473
            }
1474
        }
1475
    }
1476

1477
    /**
1478
     * Wraps the AstNode.getType() method, so that we can do extra processing on the result based on the current file
1479
     * In particular, since BrightScript does not support Unions, and there's no way to cast them to something else
1480
     * if the result of .getType() is a union, and we're in a .brs (brightScript) file, treat the result as Dynamic
1481
     *
1482
     * Also, for BrightScript parse-mode, if .getType() returns a node type, do not validate unknown members.
1483
     *
1484
     * In most cases, this returns the result of node.getType()
1485
     *
1486
     * @param file the current file being processed
1487
     * @param node the node to get the type of
1488
     * @param getTypeOpts any options to pass to node.getType()
1489
     * @returns the processed result type
1490
     */
1491
    private getNodeTypeWrapper(file: BrsFile, node: AstNode, getTypeOpts: GetTypeOptions) {
1492
        const type = node?.getType(getTypeOpts);
9,493!
1493

1494
        if (file.parseMode === ParseMode.BrightScript) {
9,493✔
1495
            // this is a brightscript file
1496
            const typeChain = getTypeOpts.typeChain;
1,049✔
1497
            if (typeChain) {
1,049✔
1498
                const hasUnion = typeChain.reduce((hasUnion, tce) => {
357✔
1499
                    return hasUnion || isUnionType(tce.type);
414✔
1500
                }, false);
1501
                if (hasUnion) {
357✔
1502
                    // there was a union somewhere in the typechain
1503
                    return DynamicType.instance;
6✔
1504
                }
1505
            }
1506
            if (isUnionType(type)) {
1,043✔
1507
                //this is a union
1508
                return DynamicType.instance;
4✔
1509
            }
1510

1511
            if (isComponentType(type)) {
1,039✔
1512
                // modify type to allow any member access for Node types
1513
                type.changeUnknownMemberToDynamic = true;
18✔
1514
            }
1515
        }
1516

1517
        // by default return the result of node.getType()
1518
        return type;
9,483✔
1519
    }
1520

1521
    private getParentTypeDescriptor(typeChainResult: TypeChainProcessResult) {
1522
        if (typeChainResult.itemParentTypeKind === BscTypeKind.NamespaceType) {
259✔
1523
            return 'namespace';
119✔
1524
        }
1525
        return 'type';
140✔
1526
    }
1527

1528
    private addDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1529
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
51!
1530
        this.event.program.diagnostics.register(diagnostic, {
51✔
1531
            tags: [diagnosticTag],
1532
            segment: this.currentSegmentBeingValidated
1533
        });
1534
    }
1535

1536
    /**
1537
     * Add a diagnostic (to the first scope) that will have `relatedInformation` for each affected scope
1538
     */
1539
    private addMultiScopeDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1540
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
548✔
1541
        this.event.program.diagnostics.register(diagnostic, {
548✔
1542
            tags: [diagnosticTag],
1543
            segment: this.currentSegmentBeingValidated,
1544
            scope: this.event.scope
1545
        });
1546
    }
1547
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc