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

rokucommunity / brighterscript / #15048

01 Jan 2026 11:17PM UTC coverage: 87.048% (-0.9%) from 87.907%
#15048

push

web-flow
Merge 02ba2bb57 into 2ea4d2108

14498 of 17595 branches covered (82.4%)

Branch coverage included in aggregate %.

192 of 261 new or added lines in 12 files covered. (73.56%)

897 existing lines in 48 files now uncovered.

15248 of 16577 relevant lines covered (91.98%)

24112.76 hits per line

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

90.9
/src/bscPlugin/validation/ScopeValidator.ts
1
import { DiagnosticTag, type Range } from 'vscode-languageserver';
1✔
2
import { isAliasStatement, isAssignmentStatement, isAssociativeArrayType, isBinaryExpression, isBooleanTypeLike, isBrsFile, isCallExpression, isCallFuncableType, isCallableType, isCallfuncExpression, isClassStatement, isClassType, isComponentType, isDottedGetExpression, isDynamicType, isEnumMemberType, isEnumType, isFunctionExpression, isFunctionParameterExpression, isLiteralExpression, isNamespaceStatement, isNamespaceType, isNewExpression, isNumberTypeLike, isObjectType, isPrimitiveType, isReferenceType, isReturnStatement, isStringTypeLike, isTypedFunctionType, isUnionType, isVariableExpression, isVoidType, isXmlScope } from '../../astUtils/reflection';
1✔
3
import type { DiagnosticInfo } from '../../DiagnosticMessages';
4
import { DiagnosticMessages } from '../../DiagnosticMessages';
1✔
5
import type { BrsFile } from '../../files/BrsFile';
6
import type { BsDiagnostic, CallableContainer, ExtraSymbolData, FileReference, GetTypeOptions, 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();
67!
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,124✔
72
    private validationKindsMetrics = new Map<string, { timeMs: number; count: number }>();
2,124✔
73

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

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

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

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

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

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

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

161
        this.event.scope.enumerateOwnFiles((file) => {
2,031✔
162
            if (isBrsFile(file)) {
2,974✔
163

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

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

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

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

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

180
                const validationVisitor = createVisitor({
1,963✔
181
                    VariableExpression: (varExpr) => {
182
                        this.addValidationKindMetric('VariableExpression', () => {
4,569✔
183
                            this.validateVariableAndDottedGetExpressions(file, varExpr);
4,569✔
184
                        });
185
                    },
186
                    DottedGetExpression: (dottedGet) => {
187
                        this.addValidationKindMetric('DottedGetExpression', () => {
1,671✔
188
                            this.validateVariableAndDottedGetExpressions(file, dottedGet);
1,671✔
189
                        });
190
                    },
191
                    CallExpression: (functionCall) => {
192
                        this.addValidationKindMetric('CallExpression', () => {
1,052✔
193
                            this.validateCallExpression(file, functionCall);
1,052✔
194
                            this.validateCreateObjectCall(file, functionCall);
1,052✔
195
                            this.validateComponentMethods(file, functionCall);
1,052✔
196
                        });
197
                    },
198
                    CallfuncExpression: (functionCall) => {
199
                        this.addValidationKindMetric('CallfuncExpression', () => {
65✔
200
                            this.validateCallFuncExpression(file, functionCall);
65✔
201
                        });
202
                    },
203
                    ReturnStatement: (returnStatement) => {
204
                        this.addValidationKindMetric('ReturnStatement', () => {
445✔
205
                            this.validateReturnStatement(file, returnStatement);
445✔
206
                        });
207
                    },
208
                    DottedSetStatement: (dottedSetStmt) => {
209
                        this.addValidationKindMetric('DottedSetStatement', () => {
103✔
210
                            this.validateDottedSetStatement(file, dottedSetStmt);
103✔
211
                        });
212
                    },
213
                    BinaryExpression: (binaryExpr) => {
214
                        this.addValidationKindMetric('BinaryExpression', () => {
352✔
215
                            this.validateBinaryExpression(file, binaryExpr);
352✔
216
                        });
217
                    },
218
                    UnaryExpression: (unaryExpr) => {
219
                        this.addValidationKindMetric('UnaryExpression', () => {
36✔
220
                            this.validateUnaryExpression(file, unaryExpr);
36✔
221
                        });
222
                    },
223
                    AssignmentStatement: (assignStmt) => {
224
                        this.addValidationKindMetric('AssignmentStatement', () => {
794✔
225
                            this.validateAssignmentStatement(file, assignStmt);
794✔
226
                            // Note: this also includes For statements
227
                            this.detectShadowedLocalVar(file, {
794✔
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,382✔
232
                            });
233
                        });
234
                    },
235
                    AugmentedAssignmentStatement: (binaryExpr) => {
236
                        this.addValidationKindMetric('AugmentedAssignmentStatement', () => {
66✔
237
                            this.validateBinaryExpression(file, binaryExpr);
66✔
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', () => {
30✔
252
                            this.detectShadowedLocalVar(file, {
30✔
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
90✔
257
                            });
258
                        });
259
                    },
260
                    FunctionParameterExpression: (funcParam) => {
261
                        this.addValidationKindMetric('FunctionParameterExpression', () => {
1,262✔
262
                            this.detectShadowedLocalVar(file, {
1,262✔
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,786✔
267
                            });
268
                        });
269
                    },
270
                    FunctionExpression: (func) => {
271
                        if (file.isTypedef) {
2,337✔
272
                            return;
10✔
273
                        }
274
                        this.addValidationKindMetric('FunctionExpression', () => {
2,327✔
275
                            this.validateFunctionExpressionForReturn(func);
2,327✔
276
                        });
277
                    },
278
                    AstNode: (node) => {
279
                        //check for doc comments
280
                        if (!node.leadingTrivia || node.leadingTrivia.filter(triviaToken => triviaToken.kind === TokenKind.Comment).length === 0) {
29,211✔
281
                            return;
22,462✔
282
                        }
283
                        this.addValidationKindMetric('AstNode', () => {
270✔
284
                            this.validateDocComments(node);
270✔
285
                        });
286
                    }
287
                });
288
                // validate only what's needed in the file
289

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

294
                let segmentsValidated = 0;
1,963✔
295

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

301

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

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

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

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

354
    private currentSegmentBeingValidated: AstNode;
355

356

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

362
    private getCircularReference(exprType: BscType) {
363
        if (exprType?.isResolvable()) {
263!
UNCOV
364
            return { isCircularReference: false };
×
365
        }
366
        if (isReferenceType(exprType)) {
263✔
367

368
            const info = exprType.getCircularReferenceInfo();
258✔
369
            return info;
258✔
370
        }
371
        return { isCircularReference: false };
5✔
372
    }
373

374

375
    /**
376
     * If this is the lhs of an assignment, we don't need to flag it as unresolved
377
     */
378
    private hasValidDeclaration(expression: Expression, exprType: BscType, definingNode?: AstNode) {
379
        if (!isVariableExpression(expression)) {
4,676✔
380
            return false;
1,215✔
381
        }
382
        let assignmentAncestor: AssignmentStatement;
383
        if (isAssignmentStatement(definingNode) && definingNode.tokens.equals.kind === TokenKind.Equal) {
3,461✔
384
            // this symbol was defined in a "normal" assignment (eg. not a compound assignment)
385
            assignmentAncestor = definingNode;
445✔
386
            return assignmentAncestor?.tokens.name?.text.toLowerCase() === expression?.tokens.name?.text.toLowerCase();
445!
387
        } else if (isFunctionParameterExpression(definingNode)) {
3,016✔
388
            // this symbol was defined in a function param
389
            return true;
624✔
390
        } else {
391
            assignmentAncestor = expression?.findAncestor(isAssignmentStatement);
2,392!
392
        }
393
        return assignmentAncestor?.tokens.name === expression?.tokens.name && isUnionType(exprType);
2,392!
394
    }
395

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

404
        //skip non CreateObject function calls
405
        const callName = util.getAllDottedGetPartsAsString(call.callee)?.toLowerCase();
1,052✔
406
        if (callName !== 'createobject' || !isLiteralExpression(call?.args[0])) {
1,052!
407
            return;
974✔
408
        }
409
        const firstParamToken = (call?.args[0] as LiteralExpression)?.tokens?.value;
78!
410
        const firstParamStringValue = firstParamToken?.text?.replace(/"/g, '');
78!
411
        if (!firstParamStringValue) {
78!
UNCOV
412
            return;
×
413
        }
414
        const firstParamStringValueLower = firstParamStringValue.toLowerCase();
78✔
415

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

450
            // Test for deprecation
451
            if (brightScriptComponent?.isDeprecated) {
39!
UNCOV
452
                this.addDiagnostic({
×
453
                    ...DiagnosticMessages.itemIsDeprecated(firstParamStringValue, brightScriptComponent.deprecatedDescription),
454
                    location: call.location
455
                });
456
            }
457
        }
458

459
    }
460

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

476
    /**
477
     * Validate every method call to `component.callfunc()`, `component.createChild()`, etc.
478
     */
479
    protected validateComponentMethods(file: BrsFile, call: CallExpression) {
480
        const lowerMethodNamesChecked = ['callfunc', 'createchild'];
1,052✔
481
        if (!isDottedGetExpression(call.callee)) {
1,052✔
482
            return;
634✔
483
        }
484

485
        const callName = call.callee.tokens?.name?.text?.toLowerCase();
418!
486
        if (!callName || !lowerMethodNamesChecked.includes(callName) || !isLiteralExpression(call?.args[0])) {
418!
487
            return;
405✔
488
        }
489

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

512

513
    private validateCallExpression(file: BrsFile, expression: CallExpression) {
514
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: {} };
1,052✔
515
        let funcType = this.getNodeTypeWrapper(file, expression?.callee, getTypeOptions);
1,052!
516
        if (funcType?.isResolvable() && isClassType(funcType)) {
1,052✔
517
            // We're calling a class - get the constructor
518
            funcType = funcType.getMemberType('new', getTypeOptions);
131✔
519
        }
520
        const callErrorLocation = expression?.callee?.location;
1,052!
521
        return this.validateFunctionCall(file, expression.callee, funcType, callErrorLocation, expression.args);
1,052✔
522

523
    }
524

525
    private validateCallFuncExpression(file: BrsFile, expression: CallfuncExpression) {
526
        const callerType = expression.callee?.getType({ flags: SymbolTypeFlag.runtime });
65!
527
        if (isDynamicType(callerType)) {
65✔
528
            return;
22✔
529
        }
530
        const methodToken = expression.tokens.methodName;
43✔
531
        const methodName = methodToken?.text ?? '';
43✔
532
        const functionFullname = `${callerType.toString()}@.${methodName}`;
43✔
533
        const callErrorLocation = expression.location;
43✔
534
        if (util.isGenericNodeType(callerType) || isObjectType(callerType) || isDynamicType(callerType)) {
43✔
535
            // ignore "general" node
536
            return;
7✔
537
        }
538

539
        const funcType = util.getCallFuncType(expression, methodToken, { flags: SymbolTypeFlag.runtime, ignoreCall: true });
36✔
540
        if (!funcType?.isResolvable()) {
33✔
541
            this.addMultiScopeDiagnostic({
10✔
542
                ...DiagnosticMessages.cannotFindCallFuncFunction(methodName, functionFullname, callerType.toString()),
543
                location: callErrorLocation
544
            });
545
        }
546

547
        return this.validateFunctionCall(file, expression, funcType, callErrorLocation, expression.args);
33✔
548
    }
549

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

591
            }
592
            if (funcType && !isCallableType(funcType) && !isReferenceType(funcType)) {
165✔
593
                const globalFuncWithVarName = globalCallableMap.get(funcName.toLowerCase());
6✔
594
                if (globalFuncWithVarName) {
6✔
595
                    funcType = globalFuncWithVarName.type;
1✔
596
                } else {
597
                    this.addMultiScopeDiagnostic({
5✔
598
                        ...DiagnosticMessages.notCallable(funcName),
599
                        location: callErrorLocation
600
                    });
601
                    return;
5✔
602
                }
603

604
            }
605
        }
606

607
        if (!isTypedFunctionType(funcType)) {
1,082✔
608
            // non typed function. nothing to check
609
            return;
293✔
610
        }
611

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

629
        let expCallArgCount = argsForCall.length;
789✔
630
        if (expCallArgCount > maxParams || expCallArgCount < minParams) {
789✔
631
            let minMaxParamsText = minParams === maxParams ? maxParams + argOffset : `${minParams + argOffset}-${maxParams + argOffset}`;
34✔
632
            this.addMultiScopeDiagnostic({
34✔
633
                ...DiagnosticMessages.mismatchArgumentCount(minMaxParamsText, expCallArgCount + argOffset),
634
                location: callErrorLocation
635
            });
636
        }
637
        let paramIndex = 0;
789✔
638
        for (let arg of argsForCall) {
789✔
639
            const data = {} as ExtraSymbolData;
680✔
640
            let argType = this.getNodeTypeWrapper(file, arg, { flags: SymbolTypeFlag.runtime, data: data });
680✔
641

642
            const paramType = funcType.params[paramIndex]?.type;
680✔
643
            if (!paramType) {
680✔
644
                // unable to find a paramType -- maybe there are more args than params
645
                break;
22✔
646
            }
647

648
            if (isCallableType(paramType) && isClassType(argType) && isClassStatement(data.definingNode)) {
658✔
649
                argType = data.definingNode.getConstructorType();
2✔
650
            }
651

652
            const compatibilityData: TypeCompatibilityData = {};
658✔
653
            const isAllowedArgConversion = this.checkAllowedArgConversions(paramType, argType);
658✔
654
            if (!isAllowedArgConversion && !paramType?.isTypeCompatible(argType, compatibilityData)) {
658!
655
                this.addMultiScopeDiagnostic({
45✔
656
                    ...DiagnosticMessages.argumentTypeMismatch(argType?.toString() ?? 'unknown', paramType?.toString() ?? 'unknown', compatibilityData),
540!
657
                    location: arg.location
658
                });
659
            }
660
            paramIndex++;
658✔
661
        }
662
    }
663

664
    private checkAllowedArgConversions(paramType: BscType, argType: BscType): boolean {
665
        if (isNumberTypeLike(argType) && isBooleanTypeLike(paramType)) {
658✔
666
            return true;
8✔
667
        }
668
        return false;
650✔
669
    }
670

671

672
    /**
673
     * Detect return statements with incompatible types vs. declared return type
674
     */
675
    private validateReturnStatement(file: BrsFile, returnStmt: ReturnStatement) {
676
        const data: ExtraSymbolData = {};
446✔
677
        const getTypeOptions = { flags: SymbolTypeFlag.runtime, data: data };
446✔
678
        let funcType = returnStmt.findAncestor(isFunctionExpression)?.getType({ flags: SymbolTypeFlag.typetime });
446✔
679
        if (isTypedFunctionType(funcType)) {
446✔
680
            let actualReturnType = returnStmt?.value
445!
681
                ? this.getNodeTypeWrapper(file, returnStmt?.value, getTypeOptions)
1,723!
682
                : VoidType.instance;
683
            const compatibilityData: TypeCompatibilityData = {};
445✔
684

685
            // `return` statement by itself in non-built-in function will actually result in `invalid`
686
            const valueReturnType = isVoidType(actualReturnType) ? InvalidType.instance : actualReturnType;
445✔
687

688
            if (funcType.returnType.isResolvable()) {
445✔
689
                if (!returnStmt?.value && isVoidType(funcType.returnType)) {
441!
690
                    // allow empty return when function is return `as void`
691
                    // eslint-disable-next-line no-useless-return
692
                    return;
8✔
693
                } else if (!funcType.returnType.isTypeCompatible(valueReturnType, compatibilityData)) {
433✔
694
                    this.addMultiScopeDiagnostic({
29✔
695
                        ...DiagnosticMessages.returnTypeMismatch(actualReturnType.toString(), funcType.returnType.toString(), compatibilityData),
696
                        location: returnStmt.value?.location ?? returnStmt.location
174✔
697
                    });
698
                }
699
            }
700
        }
701
    }
702

703
    /**
704
     * Detect assigned type different from expected member type
705
     */
706
    private validateDottedSetStatement(file: BrsFile, dottedSetStmt: DottedSetStatement) {
707
        const typeChainExpectedLHS = [] as TypeChainEntry[];
103✔
708
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
103✔
709

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

728
        let accessibilityIsOk = this.checkMemberAccessibility(file, dottedSetStmt, typeChainExpectedLHS);
59✔
729

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

739
        if (accessibilityIsOk && !expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
53!
740
            this.addMultiScopeDiagnostic({
13✔
741
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType?.toString() ?? 'unknown', expectedLHSType?.toString() ?? 'unknown', compatibilityData),
156!
742
                location: dottedSetStmt.location
743
            });
744
        }
745
    }
746

747
    /**
748
     * Detect when declared type does not match rhs type
749
     */
750
    private validateAssignmentStatement(file: BrsFile, assignStmt: AssignmentStatement) {
751
        if (!assignStmt?.typeExpression) {
794!
752
            // nothing to check
753
            return;
781✔
754
        }
755

756
        const typeChainExpectedLHS = [];
13✔
757
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
13✔
758
        const expectedLHSType = this.getNodeTypeWrapper(file, assignStmt.typeExpression, { ...getTypeOpts, data: {}, typeChain: typeChainExpectedLHS });
13✔
759
        const actualRHSType = this.getNodeTypeWrapper(file, assignStmt.value, getTypeOpts);
13✔
760
        const compatibilityData: TypeCompatibilityData = {};
13✔
761
        if (!expectedLHSType || !expectedLHSType.isResolvable()) {
13✔
762
            // LHS is not resolvable... handled elsewhere
763
        } else if (!expectedLHSType?.isTypeCompatible(actualRHSType, compatibilityData)) {
12!
764
            this.addMultiScopeDiagnostic({
1✔
765
                ...DiagnosticMessages.assignmentTypeMismatch(actualRHSType.toString(), expectedLHSType.toString(), compatibilityData),
766
                location: assignStmt.location
767
            });
768
        }
769
    }
770

771
    /**
772
     * Detect invalid use of a binary operator
773
     */
774
    private validateBinaryExpression(file: BrsFile, binaryExpr: BinaryExpression | AugmentedAssignmentStatement) {
775
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
418✔
776

777
        if (util.isInTypeExpression(binaryExpr)) {
418✔
778
            return;
49✔
779
        }
780

781
        let leftType = isBinaryExpression(binaryExpr)
369✔
782
            ? this.getNodeTypeWrapper(file, binaryExpr.left, getTypeOpts)
369✔
783
            : this.getNodeTypeWrapper(file, binaryExpr.item, getTypeOpts);
784
        let rightType = isBinaryExpression(binaryExpr)
369✔
785
            ? this.getNodeTypeWrapper(file, binaryExpr.right, getTypeOpts)
369✔
786
            : this.getNodeTypeWrapper(file, binaryExpr.value, getTypeOpts);
787

788
        if (!leftType || !rightType || !leftType.isResolvable() || !rightType.isResolvable()) {
369✔
789
            // Can not find the type. error handled elsewhere
790
            return;
13✔
791
        }
792

793
        let leftTypeToTest = leftType;
356✔
794
        let rightTypeToTest = rightType;
356✔
795

796
        if (isEnumMemberType(leftType) || isEnumType(leftType)) {
356✔
797
            leftTypeToTest = leftType.underlyingType;
11✔
798
        }
799
        if (isEnumMemberType(rightType) || isEnumType(rightType)) {
356✔
800
            rightTypeToTest = rightType.underlyingType;
10✔
801
        }
802

803
        if (isUnionType(leftType) || isUnionType(rightType)) {
356✔
804
            // TODO: it is possible to validate based on innerTypes, but more complicated
805
            // Because you need to verify each combination of types
806
            return;
7✔
807
        }
808
        const opResult = util.binaryOperatorResultType(leftTypeToTest, binaryExpr.tokens.operator, rightTypeToTest);
349✔
809

810
        if (!opResult) {
349✔
811
            // if the result was dynamic or void, that means there wasn't a valid operation
812
            this.addMultiScopeDiagnostic({
10✔
813
                ...DiagnosticMessages.operatorTypeMismatch(binaryExpr.tokens.operator.text, leftType.toString(), rightType.toString()),
814
                location: binaryExpr.location
815
            });
816
        }
817
    }
818

819
    /**
820
     * Detect invalid use of a Unary operator
821
     */
822
    private validateUnaryExpression(file: BrsFile, unaryExpr: UnaryExpression) {
823
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
36✔
824

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

827
        if (!rightType.isResolvable()) {
36!
828
            // Can not find the type. error handled elsewhere
UNCOV
829
            return;
×
830
        }
831
        let rightTypeToTest = rightType;
36✔
832
        if (isEnumMemberType(rightType)) {
36!
UNCOV
833
            rightTypeToTest = rightType.underlyingType;
×
834
        }
835

836
        if (isUnionType(rightTypeToTest)) {
36✔
837
            // TODO: it is possible to validate based on innerTypes, but more complicated
838
            // Because you need to verify each combination of types
839

840
        } else if (isDynamicType(rightTypeToTest) || isObjectType(rightTypeToTest)) {
35✔
841
            // operand is basically "any" type... ignore;
842

843
        } else if (isPrimitiveType(rightType)) {
31!
844
            const opResult = util.unaryOperatorResultType(unaryExpr.tokens.operator, rightTypeToTest);
31✔
845
            if (!opResult) {
31✔
846
                this.addMultiScopeDiagnostic({
1✔
847
                    ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
848
                    location: unaryExpr.location
849
                });
850
            }
851
        } else {
852
            // rhs is not a primitive, so no binary operator is allowed
UNCOV
853
            this.addMultiScopeDiagnostic({
×
854
                ...DiagnosticMessages.operatorTypeMismatch(unaryExpr.tokens.operator.text, rightType.toString()),
855
                location: unaryExpr.location
856
            });
857
        }
858
    }
859

860
    private validateIncrementStatement(file: BrsFile, incStmt: IncrementStatement) {
861
        const getTypeOpts = { flags: SymbolTypeFlag.runtime };
11✔
862

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

865
        if (!rightType.isResolvable()) {
11!
866
            // Can not find the type. error handled elsewhere
UNCOV
867
            return;
×
868
        }
869

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

886

887
    validateVariableAndDottedGetExpressions(file: BrsFile, expression: VariableExpression | DottedGetExpression) {
888
        if (isDottedGetExpression(expression.parent)) {
6,240✔
889
            // We validate dottedGetExpressions at the top-most level
890
            return;
1,561✔
891
        }
892
        if (isVariableExpression(expression)) {
4,679✔
893
            if (isAssignmentStatement(expression.parent) && expression.parent.tokens.name === expression.tokens.name) {
3,464!
894
                // Don't validate LHS of assignments
UNCOV
895
                return;
×
896
            } else if (isNamespaceStatement(expression.parent)) {
3,464✔
897
                return;
3✔
898
            }
899
        }
900

901
        let symbolType = SymbolTypeFlag.runtime;
4,676✔
902
        let oppositeSymbolType = SymbolTypeFlag.typetime;
4,676✔
903
        const isUsedAsType = util.isInTypeExpression(expression);
4,676✔
904
        if (isUsedAsType) {
4,676✔
905
            // This is used in a TypeExpression - only look up types from SymbolTable
906
            symbolType = SymbolTypeFlag.typetime;
1,566✔
907
            oppositeSymbolType = SymbolTypeFlag.runtime;
1,566✔
908
        }
909

910
        // Do a complete type check on all DottedGet and Variable expressions
911
        // this will create a diagnostic if an invalid member is accessed
912
        const typeChain: TypeChainEntry[] = [];
4,676✔
913
        const typeData = {} as ExtraSymbolData;
4,676✔
914
        let exprType = this.getNodeTypeWrapper(file, expression, {
4,676✔
915
            flags: symbolType,
916
            typeChain: typeChain,
917
            data: typeData
918
        });
919

920
        const hasValidDeclaration = this.hasValidDeclaration(expression, exprType, typeData?.definingNode);
4,676!
921

922
        //include a hint diagnostic if this type is marked as deprecated
923
        if (typeData.flags & SymbolTypeFlag.deprecated) { // eslint-disable-line no-bitwise
4,676✔
924
            this.addMultiScopeDiagnostic({
2✔
925
                ...DiagnosticMessages.itemIsDeprecated(),
926
                location: expression.tokens.name.location,
927
                tags: [DiagnosticTag.Deprecated]
928
            });
929
        }
930

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

965
            } else if (!(typeData?.isFromDocComment)) {
265!
966
                // only show "cannot find... " errors if the type is not defined from a doc comment
967
                const typeChainScan = util.processTypeChain(typeChain);
263✔
968
                const circularReferenceInfo = this.getCircularReference(exprType);
263✔
969
                if (isCallExpression(typeChainScan.astNode.parent) && typeChainScan.astNode.parent.callee === expression) {
263✔
970
                    this.addMultiScopeDiagnostic({
27✔
971
                        ...DiagnosticMessages.cannotFindFunction(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
972
                        location: typeChainScan?.location
81!
973
                    });
974
                } else if (circularReferenceInfo?.isCircularReference) {
236!
975
                    let diagnosticDetail = util.getCircularReferenceDiagnosticDetail(circularReferenceInfo, typeChainScan.fullNameOfItem);
9✔
976
                    this.addMultiScopeDiagnostic({
9✔
977
                        ...DiagnosticMessages.circularReferenceDetected(diagnosticDetail),
978
                        location: typeChainScan?.location
27!
979
                    });
980
                } else {
981
                    this.addMultiScopeDiagnostic({
227✔
982
                        ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem, typeChainScan.itemParentTypeName, this.getParentTypeDescriptor(typeChainScan)),
983
                        location: typeChainScan?.location
681!
984
                    });
985
                }
986

987
            }
988
        }
989
        if (isUsedAsType) {
4,676✔
990
            return;
1,566✔
991
        }
992

993
        const containingNamespace = expression.findAncestor<NamespaceStatement>(isNamespaceStatement);
3,110✔
994
        const containingNamespaceName = containingNamespace?.getName(ParseMode.BrighterScript);
3,110✔
995

996
        if (!(isCallExpression(expression.parent) && isNewExpression(expression.parent?.parent))) {
3,110!
997
            const classUsedAsVarEntry = this.checkTypeChainForClassUsedAsVar(typeChain, containingNamespaceName);
2,989✔
998
            const isClassInNamespace = containingNamespace?.getSymbolTable().hasSymbol(typeChain[0].name, SymbolTypeFlag.runtime);
2,989✔
999
            if (classUsedAsVarEntry && !isClassInNamespace) {
2,989!
1000

UNCOV
1001
                this.addMultiScopeDiagnostic({
×
1002
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable(classUsedAsVarEntry.toString()),
1003
                    location: expression.location
1004
                });
UNCOV
1005
                return;
×
1006
            }
1007
        }
1008

1009
        const lastTypeInfo = typeChain[typeChain.length - 1];
3,110✔
1010
        const parentTypeInfo = typeChain[typeChain.length - 2];
3,110✔
1011

1012
        this.checkMemberAccessibility(file, expression, typeChain);
3,110✔
1013

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

1048
    private checkTypeChainForClassUsedAsVar(typeChain: TypeChainEntry[], containingNamespaceName: string) {
1049
        const ignoreKinds = [AstNodeKind.TypecastExpression, AstNodeKind.NewExpression];
2,989✔
1050
        let lowerNameSoFar = '';
2,989✔
1051
        let classUsedAsVar;
1052
        let isFirst = true;
2,989✔
1053
        for (let i = 0; i < typeChain.length - 1; i++) { // do not look at final entry - we CAN use the constructor as a variable
2,989✔
1054
            const tce = typeChain[i];
1,448✔
1055
            lowerNameSoFar += `${lowerNameSoFar ? '.' : ''}${tce.name.toLowerCase()}`;
1,448✔
1056
            if (!isNamespaceType(tce.type)) {
1,448✔
1057
                if (isFirst && containingNamespaceName) {
739✔
1058
                    lowerNameSoFar = `${containingNamespaceName.toLowerCase()}.${lowerNameSoFar}`;
78✔
1059
                }
1060
                if (!tce.astNode || ignoreKinds.includes(tce.astNode.kind)) {
739✔
1061
                    break;
15✔
1062
                } else if (isClassType(tce.type) && lowerNameSoFar.toLowerCase() === tce.type.name.toLowerCase()) {
724✔
1063
                    classUsedAsVar = tce.type;
1✔
1064
                }
1065
                break;
724✔
1066
            }
1067
            isFirst = false;
709✔
1068
        }
1069

1070
        return classUsedAsVar;
2,989✔
1071
    }
1072

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

1103
                    // eslint-disable-next-line no-bitwise
1104
                    if (childChainItem.data.flags & SymbolTypeFlag.protected) {
153✔
1105
                        const containingClassName = containingClassStmt?.getName(ParseMode.BrighterScript);
13✔
1106
                        const containingNamespaceName = expression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
13✔
1107
                        const ancestorClasses = this.event.scope.getClassHierarchy(containingClassName, containingNamespaceName).map(link => link.item);
13✔
1108
                        const isSubClassOfDefiningClass = ancestorClasses.includes(classStmtThatDefinesChildMember);
13✔
1109

1110
                        if (!isSubClassOfDefiningClass) {
13✔
1111
                            this.addMultiScopeDiagnostic({
5✔
1112
                                ...DiagnosticMessages.memberAccessibilityMismatch(childChainItem.name, childChainItem.data.flags, definingClassName),
1113
                                location: expression.location
1114
                            });
1115
                            // there's an error... don't worry about the rest of the chain
1116
                            return false;
5✔
1117
                        }
1118
                    }
1119
                }
1120

1121
            }
1122
        }
1123
        return true;
3,160✔
1124
    }
1125

1126
    /**
1127
     * Find all "new" statements in the program,
1128
     * and make sure we can find a class with that name
1129
     */
1130
    private validateNewExpression(file: BrsFile, newExpression: NewExpression) {
1131
        const newExprType = this.getNodeTypeWrapper(file, newExpression, { flags: SymbolTypeFlag.typetime });
121✔
1132
        if (isClassType(newExprType)) {
121✔
1133
            return;
113✔
1134
        }
1135

1136
        let potentialClassName = newExpression.className.getName(ParseMode.BrighterScript);
8✔
1137
        const namespaceName = newExpression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
8!
1138
        let newableClass = this.event.scope.getClass(potentialClassName, namespaceName);
8✔
1139

1140
        if (!newableClass) {
8!
1141
            //try and find functions with this name.
1142
            let fullName = util.getFullyQualifiedClassName(potentialClassName, namespaceName);
8✔
1143

1144
            this.addMultiScopeDiagnostic({
8✔
1145
                ...DiagnosticMessages.expressionIsNotConstructable(fullName),
1146
                location: newExpression.className.location
1147
            });
1148

1149
        }
1150
    }
1151

1152
    private validateFunctionExpressionForReturn(func: FunctionExpression) {
1153
        const returnType = func?.returnTypeExpression?.getType({ flags: SymbolTypeFlag.typetime });
2,327!
1154

1155
        if (!returnType || !returnType.isResolvable() || isVoidType(returnType) || isDynamicType(returnType)) {
2,327✔
1156
            return;
2,046✔
1157
        }
1158
        const returns = func.body?.findChild<ReturnStatement>(isReturnStatement, { walkMode: WalkMode.visitAll });
281!
1159
        if (!returns && isStringTypeLike(returnType)) {
281✔
1160
            this.addMultiScopeDiagnostic({
5✔
1161
                ...DiagnosticMessages.returnTypeCoercionMismatch(returnType.toString()),
1162
                location: func.location
1163
            });
1164
        }
1165
    }
1166

1167
    /**
1168
     * Create diagnostics for any duplicate function declarations
1169
     */
1170
    private flagDuplicateFunctionDeclarations() {
1171
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration });
2,028✔
1172

1173
        //for each list of callables with the same name
1174
        for (let [lowerName, callableContainers] of this.event.scope.getCallableContainerMap()) {
2,028✔
1175

1176
            let globalCallables = [] as CallableContainer[];
152,282✔
1177
            let nonGlobalCallables = [] as CallableContainer[];
152,282✔
1178
            let ownCallables = [] as CallableContainer[];
152,282✔
1179
            let ancestorNonGlobalCallables = [] as CallableContainer[];
152,282✔
1180

1181

1182
            for (let container of callableContainers) {
152,282✔
1183
                if (container.scope === this.event.program.globalScope) {
158,400✔
1184
                    globalCallables.push(container);
156,156✔
1185
                } else {
1186
                    nonGlobalCallables.push(container);
2,244✔
1187
                    if (container.scope === this.event.scope) {
2,244✔
1188
                        ownCallables.push(container);
2,214✔
1189
                    } else {
1190
                        ancestorNonGlobalCallables.push(container);
30✔
1191
                    }
1192
                }
1193
            }
1194

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

1219
            //add error diagnostics about duplicate functions in the same scope
1220
            if (ownCallables.length > 1) {
152,282✔
1221

1222
                for (let callableContainer of ownCallables) {
5✔
1223
                    let callable = callableContainer.callable;
10✔
1224
                    const related = [];
10✔
1225
                    for (const ownCallable of ownCallables) {
10✔
1226
                        const thatNameRange = ownCallable.callable.nameRange;
20✔
1227
                        if (ownCallable.callable.nameRange !== callable.nameRange) {
20✔
1228
                            related.push({
10✔
1229
                                message: `Function declared here`,
1230
                                location: util.createLocationFromRange(
1231
                                    util.pathToUri(ownCallable.callable.file?.srcPath),
30!
1232
                                    thatNameRange
1233
                                )
1234
                            });
1235
                        }
1236
                    }
1237

1238
                    this.addMultiScopeDiagnostic({
10✔
1239
                        ...DiagnosticMessages.duplicateFunctionImplementation(callable.name),
1240
                        location: util.createLocationFromFileRange(callable.file, callable.nameRange),
1241
                        relatedInformation: related
1242
                    }, ScopeValidatorDiagnosticTag.DuplicateFunctionDeclaration);
1243
                }
1244
            }
1245
        }
1246
    }
1247

1248
    /**
1249
     * Verify that all of the scripts imported by each file in this scope actually exist, and have the correct case
1250
     */
1251
    private validateScriptImportPaths() {
1252
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Imports });
2,028✔
1253

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

1271
                this.addMultiScopeDiagnostic({
15✔
1272
                    ...dInfo,
1273
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
1274
                }, ScopeValidatorDiagnosticTag.Imports);
1275
                //if the character casing of the script import path does not match that of the actual path
1276
            } else if (scriptImport.destPath !== referencedFile.destPath) {
590✔
1277
                this.addMultiScopeDiagnostic({
2✔
1278
                    ...DiagnosticMessages.scriptImportCaseMismatch(referencedFile.destPath),
1279
                    location: util.createLocationFromFileRange(scriptImport.sourceFile, scriptImport.filePathRange)
1280
                }, ScopeValidatorDiagnosticTag.Imports);
1281
            }
1282
        }
1283
    }
1284

1285
    /**
1286
     * Validate all classes defined in this scope
1287
     */
1288
    private validateClasses() {
1289
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, tag: ScopeValidatorDiagnosticTag.Classes });
2,028✔
1290

1291
        let validator = new BsClassValidator(this.event.scope);
2,028✔
1292
        validator.validate();
2,028✔
1293
        for (const diagnostic of validator.diagnostics) {
2,028✔
1294
            this.addMultiScopeDiagnostic({
29✔
1295
                ...diagnostic
1296
            }, ScopeValidatorDiagnosticTag.Classes);
1297
        }
1298
    }
1299

1300

1301
    /**
1302
     * Find various function collisions
1303
     */
1304
    private diagnosticDetectFunctionCollisions(file: BrsFile) {
1305
        const fileUri = util.pathToUri(file.srcPath);
2,383✔
1306
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: fileUri, tag: ScopeValidatorDiagnosticTag.FunctionCollisions });
2,383✔
1307
        for (let func of file.callables) {
2,383✔
1308
            const funcName = func.getName(ParseMode.BrighterScript);
2,173✔
1309
            const lowerFuncName = funcName?.toLowerCase();
2,173!
1310
            if (lowerFuncName) {
2,173!
1311

1312
                //find function declarations with the same name as a stdlib function
1313
                if (globalCallableMap.has(lowerFuncName)) {
2,173✔
1314
                    this.addMultiScopeDiagnostic({
5✔
1315
                        ...DiagnosticMessages.scopeFunctionShadowedByBuiltInFunction(),
1316
                        location: util.createLocationFromRange(fileUri, func.nameRange)
1317

1318
                    });
1319
                }
1320
            }
1321
        }
1322
    }
1323

1324
    public detectShadowedLocalVar(file: BrsFile, varDeclaration: { expr: AstNode; name: string; type: BscType; nameRange: Range }) {
1325
        const varName = varDeclaration.name;
2,086✔
1326
        const lowerVarName = varName.toLowerCase();
2,086✔
1327
        const callableContainerMap = this.event.scope.getCallableContainerMap();
2,086✔
1328
        const containingNamespace = varDeclaration.expr?.findAncestor<NamespaceStatement>(isNamespaceStatement);
2,086!
1329
        const localVarIsInNamespace = util.isVariableMemberOfNamespace(varDeclaration.name, varDeclaration.expr, containingNamespace);
2,086✔
1330

1331
        const varIsFunction = () => {
2,086✔
1332
            return isCallableType(varDeclaration.type) && !isDynamicType(varDeclaration.type);
10✔
1333
        };
1334

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

1393
    private validateXmlInterface(scope: XmlScope) {
1394
        if (!scope.xmlFile.parser.ast?.componentElement?.interfaceElement) {
538!
1395
            return;
449✔
1396
        }
1397
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLInterface });
89!
1398

1399
        const iface = scope.xmlFile.parser.ast.componentElement.interfaceElement;
89✔
1400
        const callableContainerMap = scope.getCallableContainerMap();
89✔
1401
        //validate functions
1402
        for (const func of iface.functions) {
89✔
1403
            const name = func.name;
78✔
1404
            if (!name) {
78✔
1405
                this.addDiagnostic({
3✔
1406
                    ...DiagnosticMessages.xmlTagMissingAttribute(func.tokens.startTagName.text, 'name'),
1407
                    location: func.tokens.startTagName.location
1408
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1409
            } else if (!callableContainerMap.has(name.toLowerCase())) {
75✔
1410
                this.addDiagnostic({
4✔
1411
                    ...DiagnosticMessages.xmlFunctionNotFound(name),
1412
                    location: func.getAttribute('name')?.tokens.value.location
12!
1413
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1414
            }
1415
        }
1416
        //validate fields
1417
        for (const field of iface.fields) {
89✔
1418
            const { id, type, onChange } = field;
46✔
1419
            if (!id) {
46✔
1420
                this.addDiagnostic({
3✔
1421
                    ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'id'),
1422
                    location: field.tokens.startTagName.location
1423
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1424
            }
1425
            if (!type) {
46✔
1426
                if (!field.alias) {
3✔
1427
                    this.addDiagnostic({
2✔
1428
                        ...DiagnosticMessages.xmlTagMissingAttribute(field.tokens.startTagName.text, 'type'),
1429
                        location: field.tokens.startTagName.location
1430
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1431
                }
1432
            } else if (!SGFieldTypes.includes(type.toLowerCase())) {
43✔
1433
                this.addDiagnostic({
1✔
1434
                    ...DiagnosticMessages.xmlInvalidFieldType(type),
1435
                    location: field.getAttribute('type')?.tokens.value.location
3!
1436
                }, ScopeValidatorDiagnosticTag.XMLInterface);
1437
            }
1438
            if (onChange) {
46✔
1439
                if (!callableContainerMap.has(onChange.toLowerCase())) {
1!
1440
                    this.addDiagnostic({
1✔
1441
                        ...DiagnosticMessages.xmlFunctionNotFound(onChange),
1442
                        location: field.getAttribute('onchange')?.tokens.value.location
3!
1443
                    }, ScopeValidatorDiagnosticTag.XMLInterface);
1444
                }
1445
            }
1446
        }
1447
    }
1448

1449
    private validateDocComments(node: AstNode) {
1450
        const doc = brsDocParser.parseNode(node);
270✔
1451
        for (const docTag of doc.tags) {
270✔
1452
            const docTypeTag = docTag as BrsDocWithType;
31✔
1453
            if (!docTypeTag.typeExpression || !docTypeTag.location) {
31✔
1454
                continue;
2✔
1455
            }
1456
            const foundType = docTypeTag.typeExpression?.getType({ flags: SymbolTypeFlag.typetime });
29!
1457
            if (!foundType?.isResolvable()) {
29!
1458
                this.addMultiScopeDiagnostic({
8✔
1459
                    ...DiagnosticMessages.cannotFindName(docTypeTag.typeString),
1460
                    location: brsDocParser.getTypeLocationFromToken(docTypeTag.token) ?? docTypeTag.location
24!
1461
                });
1462
            }
1463
        }
1464
    }
1465

1466
    /**
1467
     * Detect when a child has imported a script that an ancestor also imported
1468
     */
1469
    private diagnosticDetectDuplicateAncestorScriptImports(scope: XmlScope) {
1470
        this.event.program.diagnostics.clearByFilter({ scope: this.event.scope, fileUri: util.pathToUri(scope.xmlFile?.srcPath), tag: ScopeValidatorDiagnosticTag.XMLImports });
538!
1471
        if (scope.xmlFile.parentComponent) {
538✔
1472
            //build a lookup of pkg paths -> FileReference so we can more easily look up collisions
1473
            let parentScriptImports = scope.xmlFile.getAncestorScriptTagImports();
34✔
1474
            let lookup = {} as Record<string, FileReference>;
34✔
1475
            for (let parentScriptImport of parentScriptImports) {
34✔
1476
                //keep the first occurance of a pkgPath. Parent imports are first in the array
1477
                if (!lookup[parentScriptImport.destPath]) {
30!
1478
                    lookup[parentScriptImport.destPath] = parentScriptImport;
30✔
1479
                }
1480
            }
1481

1482
            //add warning for every script tag that this file shares with an ancestor
1483
            for (let scriptImport of scope.xmlFile.scriptTagImports) {
34✔
1484
                let ancestorScriptImport = lookup[scriptImport.destPath];
30✔
1485
                if (ancestorScriptImport) {
30✔
1486
                    let ancestorComponent = ancestorScriptImport.sourceFile as XmlFile;
21✔
1487
                    let ancestorComponentName = ancestorComponent.componentName?.text ?? ancestorComponent.destPath;
21!
1488
                    this.addDiagnostic({
21✔
1489
                        location: util.createLocationFromFileRange(scope.xmlFile, scriptImport.filePathRange),
1490
                        ...DiagnosticMessages.unnecessaryScriptImportInChildFromParent(ancestorComponentName)
1491
                    }, ScopeValidatorDiagnosticTag.XMLImports);
1492
                }
1493
            }
1494
        }
1495
    }
1496

1497
    /**
1498
     * Wraps the AstNode.getType() method, so that we can do extra processing on the result based on the current file
1499
     * In particular, since BrightScript does not support Unions, and there's no way to cast them to something else
1500
     * if the result of .getType() is a union, and we're in a .brs (brightScript) file, treat the result as Dynamic
1501
     *
1502
     * Also, for BrightScript parse-mode, if .getType() returns a node type, do not validate unknown members.
1503
     *
1504
     * In most cases, this returns the result of node.getType()
1505
     *
1506
     * @param file the current file being processed
1507
     * @param node the node to get the type of
1508
     * @param getTypeOpts any options to pass to node.getType()
1509
     * @returns the processed result type
1510
     */
1511
    private getNodeTypeWrapper(file: BrsFile, node: AstNode, getTypeOpts: GetTypeOptions) {
1512
        const type = node?.getType(getTypeOpts);
10,333!
1513

1514
        if (file.parseMode === ParseMode.BrightScript) {
10,333✔
1515
            // this is a brightscript file
1516
            const typeChain = getTypeOpts.typeChain;
1,059✔
1517
            if (typeChain) {
1,059✔
1518
                const hasUnion = typeChain.reduce((hasUnion, tce) => {
360✔
1519
                    return hasUnion || isUnionType(tce.type);
417✔
1520
                }, false);
1521
                if (hasUnion) {
360✔
1522
                    // there was a union somewhere in the typechain
1523
                    return DynamicType.instance;
1✔
1524
                }
1525
            }
1526
            if (isUnionType(type)) {
1,058!
1527
                //this is a union
UNCOV
1528
                return DynamicType.instance;
×
1529
            }
1530

1531
            if (isComponentType(type)) {
1,058✔
1532
                // modify type to allow any member access for Node types
1533
                type.changeUnknownMemberToDynamic = true;
18✔
1534
            }
1535
        }
1536

1537
        // by default return the result of node.getType()
1538
        return type;
10,332✔
1539
    }
1540

1541
    private getParentTypeDescriptor(typeChainResult: TypeChainProcessResult) {
1542
        if (typeChainResult.itemParentTypeKind === BscTypeKind.NamespaceType) {
261✔
1543
            return 'namespace';
119✔
1544
        }
1545
        return 'type';
142✔
1546
    }
1547

1548
    private addDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1549
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
51!
1550
        this.event.program.diagnostics.register(diagnostic, {
51✔
1551
            tags: [diagnosticTag],
1552
            segment: this.currentSegmentBeingValidated
1553
        });
1554
    }
1555

1556
    /**
1557
     * Add a diagnostic (to the first scope) that will have `relatedInformation` for each affected scope
1558
     */
1559
    private addMultiScopeDiagnostic(diagnostic: BsDiagnostic, diagnosticTag?: string) {
1560
        diagnosticTag = diagnosticTag ?? (this.currentSegmentBeingValidated ? ScopeValidatorDiagnosticTag.Segment : ScopeValidatorDiagnosticTag.Default);
565✔
1561
        this.event.program.diagnostics.register(diagnostic, {
565✔
1562
            tags: [diagnosticTag],
1563
            segment: this.currentSegmentBeingValidated,
1564
            scope: this.event.scope
1565
        });
1566
    }
1567
}
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