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

rokucommunity / brighterscript / #15591

27 Apr 2026 05:09PM UTC coverage: 89.052% (+0.02%) from 89.035%
#15591

push

web-flow
Merge 50146fe71 into 6aafd06cc

8170 of 9670 branches covered (84.49%)

Branch coverage included in aggregate %.

64 of 67 new or added lines in 2 files covered. (95.52%)

10392 of 11174 relevant lines covered (93.0%)

2000.87 hits per line

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

88.64
/src/bscPlugin/validation/ScopeValidator.ts
1
import { URI } from 'vscode-uri';
1✔
2
import { isBrsFile, isCallExpression, isDottedGetExpression, isLiteralExpression, isNamespaceStatement, isVariableExpression, isXmlScope } from '../../astUtils/reflection';
1✔
3
import { Cache } from '../../Cache';
1✔
4
import { DiagnosticMessages } from '../../DiagnosticMessages';
1✔
5
import type { BrsFile } from '../../files/BrsFile';
6
import type { BscFile, BsDiagnostic, OnScopeValidateEvent } from '../../interfaces';
7
import type { ConstStatement, EnumStatement, NamespaceStatement } from '../../parser/Statement';
8
import util from '../../util';
1✔
9
import { nodes, components } from '../../roku-types';
1✔
10
import type { BRSComponentData } from '../../roku-types';
11
import type { Token } from '../../lexer/Token';
12
import { TokenKind } from '../../lexer/TokenKind';
1✔
13
import type { Scope } from '../../Scope';
14
import type { DiagnosticRelatedInformation } from 'vscode-languageserver';
15
import type { Expression } from '../../parser/AstNode';
16
import type { VariableExpression, DottedGetExpression } from '../../parser/Expression';
17
import { ParseMode } from '../../parser/Parser';
1✔
18
import { createVisitor, WalkMode } from '../../astUtils/visitors';
1✔
19

20
/**
21
 * The lower-case names of all platform-included scenegraph nodes
22
 */
23
const platformNodeNames = new Set(Object.values(nodes).map(x => x.name.toLowerCase()));
97✔
24
const platformComponentNames = new Set(Object.values(components).map(x => x.name.toLowerCase()));
67✔
25

26
/**
27
 * A validator that handles all scope validations for a program validation cycle.
28
 * You should create ONE of these to handle all scope events between beforeProgramValidate and afterProgramValidate,
29
 * and call reset() before using it again in the next cycle
30
 */
31
export class ScopeValidator {
1✔
32

33
    /**
34
     * The event currently being processed. This will change multiple times throughout the lifetime of this validator
35
     */
36
    private event: OnScopeValidateEvent;
37

38
    public processEvent(event: OnScopeValidateEvent) {
39
        this.event = event;
2,478✔
40
        this.walkFiles();
2,478✔
41
        this.detectDuplicateEnums();
2,478✔
42
        this.detectCircularConstReferences();
2,478✔
43
    }
44

45
    public reset() {
46
        this.event = undefined;
943✔
47
        this.onceCache.clear();
943✔
48
        this.multiScopeCache.clear();
943✔
49
    }
50

51
    private walkFiles() {
52
        this.event.scope.enumerateOwnFiles((file) => {
2,478✔
53
            if (isBrsFile(file)) {
2,726✔
54
                this.iterateFileExpressions(file);
2,514✔
55
                this.validateCreateObjectCalls(file);
2,514✔
56
                this.validateComputedAAKeys(file);
2,514✔
57
            }
58
        });
59
    }
60

61
    private validateComputedAAKeys(file: BrsFile) {
62
        const { scope } = this.event;
2,514✔
63
        file.ast.walk(createVisitor({
2,514✔
64
            AAIndexedMemberExpression: (member) => {
65
                // Direct string literal (e.g. ["my-key"]) is valid
66
                if (isLiteralExpression(member.key)) {
23✔
67
                    if (member.key.token.kind !== TokenKind.StringLiteral) {
4✔
68
                        this.addMultiScopeDiagnostic({
3✔
69
                            file: file,
70
                            ...DiagnosticMessages.computedAAKeyMustBeStringExpression(),
71
                            range: member.key.range
72
                        });
73
                    }
74
                    return;
4✔
75
                }
76
                const parts = util.getDottedGetPath(member.key);
19✔
77
                if (parts.length === 0) {
19✔
78
                    this.addMultiScopeDiagnostic({
1✔
79
                        file: file,
80
                        ...DiagnosticMessages.computedPropertyKeyMustBeConstantExpression(),
81
                        range: member.key.range
82
                    });
83
                    return;
1✔
84
                }
85
                const enclosingNamespace = member.key.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript)?.toLowerCase();
18✔
86
                const entityName = parts.map(p => p.name.text.toLowerCase()).join('.');
29✔
87
                // Check enum member
88
                const memberLink = scope.getEnumMemberFileLink(entityName, enclosingNamespace);
18✔
89
                if (memberLink) {
18✔
90
                    const value = memberLink.item.getValue();
9✔
91
                    if (!value?.startsWith('"')) {
9!
92
                        this.addMultiScopeDiagnostic({
2✔
93
                            file: file,
94
                            ...DiagnosticMessages.computedAAKeyMustBeStringExpression(),
95
                            range: member.key.range
96
                        });
97
                    }
98
                    return;
9✔
99
                }
100
                // Check const — follow the chain to find the root literal type
101
                const constLink = scope.getConstFileLink(entityName, enclosingNamespace);
9✔
102
                if (constLink) {
9✔
103
                    if (!this.constResolvesToString(constLink.item.value, enclosingNamespace, scope)) {
7✔
104
                        this.addMultiScopeDiagnostic({
4✔
105
                            file: file,
106
                            ...DiagnosticMessages.computedAAKeyMustBeStringExpression(),
107
                            range: member.key.range
108
                        });
109
                    }
110
                    return;
7✔
111
                }
112
                this.addMultiScopeDiagnostic({
2✔
113
                    file: file,
114
                    ...DiagnosticMessages.computedPropertyKeyMustBeConstantExpression(),
115
                    range: member.key.range
116
                });
117
            }
118
        }), { walkMode: WalkMode.visitAllRecursive });
119
    }
120

121
    /**
122
     * Recursively resolve a const/enum reference to determine if its ultimate value is a string.
123
     * Returns true only if the value is confirmed to be a string.
124
     */
125
    private constResolvesToString(value: Expression, enclosingNamespace: string, scope: Scope, visited = new Set<string>()): boolean {
7✔
126
        if (isLiteralExpression(value)) {
12✔
127
            return value.token.kind === TokenKind.StringLiteral;
6✔
128
        }
129
        const parts = util.getDottedGetPath(value);
6✔
130
        if (parts.length === 0) {
6!
131
            return false;
×
132
        }
133
        const entityName = parts.map(p => p.name.text.toLowerCase()).join('.');
7✔
134
        if (visited.has(entityName)) {
6✔
135
            return false; // circular reference — cannot confirm string
1✔
136
        }
137
        visited.add(entityName);
5✔
138
        const constLink = scope.getConstFileLink(entityName, enclosingNamespace);
5✔
139
        if (constLink) {
5✔
140
            return this.constResolvesToString(constLink.item.value, enclosingNamespace, scope, visited);
4✔
141
        }
142
        const memberLink = scope.getEnumMemberFileLink(entityName, enclosingNamespace);
1✔
143
        if (memberLink) {
1!
144
            return this.constResolvesToString(memberLink.item.value, enclosingNamespace, scope, visited);
1✔
145
        }
146
        return false;
×
147
    }
148

149
    private expressionsByFile = new Cache<BrsFile, Readonly<ExpressionInfo>[]>();
1,500✔
150
    private iterateFileExpressions(file: BrsFile) {
151
        const { scope } = this.event;
2,514✔
152
        //build an expression collection ONCE per file
153
        const expressionInfos = this.expressionsByFile.getOrAdd(file, () => {
2,514✔
154
            const result: DeepWriteable<ExpressionInfo[]> = [];
2,426✔
155
            const expressions = [
2,426✔
156
                ...file.parser.references.expressions,
157
                //all class "extends <whatever>" expressions
158
                ...file.parser.references.classStatements.map(x => x.parentClassName?.expression),
200✔
159
                //all interface "extends <whatever>" expressions
160
                ...file.parser.references.interfaceStatements.map(x => x.parentInterfaceName?.expression)
38✔
161
            ];
162
            for (let expression of expressions) {
2,426✔
163
                if (!expression) {
2,946✔
164
                    continue;
169✔
165
                }
166

167
                //walk left-to-right on every expression, only keep the ones that start with VariableExpression, and then keep subsequent DottedGet parts
168
                const parts = util.getDottedGetPath(expression);
2,777✔
169

170
                if (parts.length > 0) {
2,777✔
171
                    result.push({
875✔
172
                        parts: parts,
173
                        expression: expression,
174
                        enclosingNamespaceNameLower: expression.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript)?.toLowerCase()
5,250✔
175
                    });
176
                }
177
            }
178
            return result as unknown as Readonly<ExpressionInfo>[];
2,426✔
179
        });
180

181
        outer:
2,514✔
182
        for (const info of expressionInfos) {
2,514✔
183
            const symbolTable = info.expression.getSymbolTable();
900✔
184
            const firstPart = info.parts[0];
900✔
185
            const firstNamespacePart = info.parts[0].name.text;
900✔
186
            const firstNamespacePartLower = firstNamespacePart?.toLowerCase();
900!
187
            //get the namespace container (accounting for namespace-relative as well)
188
            const namespaceContainer = scope.getNamespace(firstNamespacePartLower, info.enclosingNamespaceNameLower);
900✔
189

190
            //flag all unknown left-most variables
191
            if (
900✔
192
                !symbolTable?.hasSymbol(firstPart.name?.text) &&
6,382!
193
                !namespaceContainer
194
            ) {
195
                //flag functions differently than all other variables
196
                if (isCallExpression(firstPart.parent) && firstPart.parent.callee === firstPart) {
80✔
197
                    this.addMultiScopeDiagnostic({
24✔
198
                        file: file as BscFile,
199
                        ...DiagnosticMessages.cannotFindFunction(firstPart.name?.text),
72!
200
                        range: firstPart.name.range
201
                    });
202
                } else {
203
                    this.addMultiScopeDiagnostic({
56✔
204
                        file: file as BscFile,
205
                        ...DiagnosticMessages.cannotFindName(firstPart.name?.text),
168!
206
                        range: firstPart.name.range
207
                    });
208
                }
209
                //skip to the next expression
210
                continue;
80✔
211
            }
212

213
            const enumStatement = scope.getEnum(firstNamespacePartLower, info.enclosingNamespaceNameLower);
820✔
214

215
            //if this isn't a namespace, skip it
216
            if (!namespaceContainer && !enumStatement) {
820✔
217
                continue;
632✔
218
            }
219

220
            //catch unknown namespace items
221
            let entityName = firstNamespacePart;
188✔
222
            let entityNameLower = firstNamespacePart.toLowerCase();
188✔
223
            for (let i = 1; i < info.parts.length; i++) {
188✔
224
                const part = info.parts[i];
240✔
225
                entityName += '.' + part.name.text;
240✔
226
                entityNameLower += '.' + part.name.text.toLowerCase();
240✔
227

228
                //if this is an enum member, stop validating here to prevent errors further down the chain
229
                if (scope.getEnumMemberFileLink(entityName, info.enclosingNamespaceNameLower)) {
240✔
230
                    break;
81✔
231
                }
232

233
                if (
159✔
234
                    !scope.getEnumMap().has(entityNameLower) &&
562✔
235
                    !scope.getClassMap().has(entityNameLower) &&
236
                    !scope.getConstMap().has(entityNameLower) &&
237
                    !scope.getCallableByName(entityNameLower) &&
238
                    !scope.getNamespace(entityNameLower, info.enclosingNamespaceNameLower)
239
                ) {
240
                    //if this looks like an enum, provide a nicer error message
241
                    const theEnum = this.getEnum(scope, entityNameLower)?.item;
24✔
242
                    if (theEnum) {
24✔
243
                        this.addMultiScopeDiagnostic({
14✔
244
                            file: file,
245
                            ...DiagnosticMessages.unknownEnumValue(part.name.text?.split('.').pop(), theEnum.fullName),
42!
246
                            range: part.name.range,
247
                            relatedInformation: [{
248
                                message: 'Enum declared here',
249
                                location: util.createLocation(
250
                                    URI.file(file.srcPath).toString(),
251
                                    theEnum.tokens.name.range
252
                                )
253
                            }]
254
                        });
255
                    } else {
256
                        //flag functions differently than all other variables
257
                        if (isCallExpression(firstPart.parent) && firstPart.parent.callee === firstPart) {
10!
258
                            this.addMultiScopeDiagnostic({
×
259
                                ...DiagnosticMessages.cannotFindFunction(part.name.text, entityName),
260
                                range: part.name.range,
261
                                file: file
262
                            });
263
                        } else {
264
                            this.addMultiScopeDiagnostic({
10✔
265
                                ...DiagnosticMessages.cannotFindName(part.name.text, entityName),
266
                                range: part.name.range,
267
                                file: file
268
                            });
269
                        }
270
                    }
271
                    //no need to add another diagnostic for future unknown items
272
                    continue outer;
24✔
273
                }
274
            }
275
            //if the full expression is a namespace path, this is an illegal statement because namespaces don't exist at runtme
276
            if (scope.getNamespace(entityNameLower, info.enclosingNamespaceNameLower)) {
164✔
277
                this.addMultiScopeDiagnostic({
8✔
278
                    ...DiagnosticMessages.itemCannotBeUsedAsVariable('namespace'),
279
                    range: info.expression.range,
280
                    file: file
281
                }, 'When used in scope');
282
            }
283
        }
284
    }
285

286
    /**
287
     * Given a string optionally separated by dots, find an enum related to it.
288
     * For example, all of these would return the enum: `SomeNamespace.SomeEnum.SomeMember`, SomeEnum.SomeMember, `SomeEnum`
289
     */
290
    private getEnum(scope: Scope, name: string) {
291
        //look for the enum directly
292
        let result = scope.getEnumMap().get(name);
24✔
293

294
        //assume we've been given the enum.member syntax, so pop the member and try again
295
        if (!result) {
24!
296
            const parts = name.split('.');
24✔
297
            parts.pop();
24✔
298
            result = scope.getEnumMap().get(parts.join('.'));
24✔
299
        }
300
        return result;
24✔
301
    }
302

303
    /**
304
     * Flag duplicate enums
305
     */
306
    private detectDuplicateEnums() {
307
        const diagnostics: BsDiagnostic[] = [];
2,478✔
308
        const enumLocationsByName = new Cache<string, Array<{ file: BrsFile; statement: EnumStatement }>>();
2,478✔
309
        this.event.scope.enumerateBrsFiles((file) => {
2,478✔
310
            for (const enumStatement of file.parser.references.enumStatements) {
2,524✔
311
                const fullName = enumStatement.fullName;
99✔
312
                const nameLower = fullName?.toLowerCase();
99!
313
                if (nameLower?.length > 0) {
99!
314
                    enumLocationsByName.getOrAdd(nameLower, () => []).push({
99✔
315
                        file: file,
316
                        statement: enumStatement
317
                    });
318
                }
319
            }
320
        });
321

322
        //now that we've collected all enum declarations, flag duplicates
323
        for (const enumLocations of enumLocationsByName.values()) {
2,478✔
324
            //sort by srcPath to keep the primary enum location consistent
325
            enumLocations.sort((a, b) => {
97✔
326
                const pathA = a.file?.srcPath;
2!
327
                const pathB = b.file?.srcPath;
2!
328
                if (pathA < pathB) {
2✔
329
                    return -1;
1✔
330
                } else if (pathA > pathB) {
1!
331
                    return 1;
×
332
                }
333
                return 0;
1✔
334
            });
335
            const primaryEnum = enumLocations.shift();
97✔
336
            const fullName = primaryEnum.statement.fullName;
97✔
337
            for (const duplicateEnumInfo of enumLocations) {
97✔
338
                diagnostics.push({
2✔
339
                    ...DiagnosticMessages.duplicateEnumDeclaration(this.event.scope.name, fullName),
340
                    file: duplicateEnumInfo.file,
341
                    range: duplicateEnumInfo.statement.tokens.name.range,
342
                    relatedInformation: [{
343
                        message: 'Enum declared here',
344
                        location: util.createLocation(
345
                            URI.file(primaryEnum.file.srcPath).toString(),
346
                            primaryEnum.statement.tokens.name.range
347
                        )
348
                    }]
349
                });
350
            }
351
        }
352
        this.event.scope.addDiagnostics(diagnostics);
2,478✔
353
    }
354

355
    /**
356
     * Flag circular references between consts (e.g. const A = B; const B = A, or
357
     * aggregate cycles like const A = { x: B }; const B = { y: A }). Without this
358
     * check, the transpile pass silently emits unresolved refs at the cycle break
359
     * point, producing code that fails at runtime.
360
     */
361
    private detectCircularConstReferences() {
362
        const scope = this.event.scope;
2,478✔
363
        const diagnostics: BsDiagnostic[] = [];
2,478✔
364
        const fullyValidated = new Set<ConstStatement>();
2,478✔
365
        const reportedCycleStarts = new Set<ConstStatement>();
2,478✔
366

367
        const followConstRef = (
2,478✔
368
            expression: Expression,
369
            namespace: string | undefined,
370
            chain: ConstStatement[]
371
        ) => {
372
            const parts = util.splitExpression(expression);
51✔
373
            const processedNames: string[] = [];
51✔
374
            for (const part of parts) {
51✔
375
                if (!isVariableExpression(part) && !isDottedGetExpression(part)) {
91!
NEW
376
                    return;
×
377
                }
378
                processedNames.push(part.name?.text?.toLowerCase());
91!
379
                const link = scope.getConstFileLink(processedNames.join('.'), namespace);
91✔
380
                if (link) {
91✔
381
                    walkConst(link.item, link.file, chain);
38✔
382
                    return;
38✔
383
                }
384
            }
385
        };
386

387
        const walkConst = (
2,478✔
388
            constStatement: ConstStatement,
389
            file: BrsFile,
390
            chain: ConstStatement[]
391
        ) => {
392
            const cycleStart = chain.indexOf(constStatement);
135✔
393
            if (cycleStart >= 0) {
135✔
394
                //emit one diagnostic per distinct cycle (keyed on the first const in the cycle).
395
                //A 3-cycle A->B->C->A is otherwise reported 3 times (once per starting node).
396
                const cycleNodes = chain.slice(cycleStart);
6✔
397
                const canonicalStart = cycleNodes.reduce((min, c) => {
6✔
398
                    return (c.fullName ?? '') < (min.fullName ?? '') ? c : min;
14!
399
                }, cycleNodes[0]);
400
                if (reportedCycleStarts.has(canonicalStart)) {
6!
NEW
401
                    return;
×
402
                }
403
                reportedCycleStarts.add(canonicalStart);
6✔
404
                const items = cycleNodes.map(c => c.fullName).concat(constStatement.fullName);
14✔
405
                diagnostics.push({
6✔
406
                    ...DiagnosticMessages.circularReferenceDetected(items, scope.name),
407
                    file: file,
408
                    range: constStatement.tokens.name.range
409
                });
410
                return;
6✔
411
            }
412
            if (fullyValidated.has(constStatement)) {
129✔
413
                return;
32✔
414
            }
415
            chain.push(constStatement);
97✔
416
            const innerNamespace = constStatement.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript);
97✔
417
            const value = constStatement.value;
97✔
418
            if (value) {
97!
419
                if (isVariableExpression(value) || isDottedGetExpression(value)) {
97✔
420
                    followConstRef(value, innerNamespace, chain);
18✔
421
                } else {
422
                    value.walk(createVisitor({
79✔
423
                        VariableExpression: (varExpr) => {
424
                            if (isDottedGetExpression(varExpr.parent)) {
33✔
425
                                return;
23✔
426
                            }
427
                            followConstRef(varExpr, innerNamespace, chain);
10✔
428
                        },
429
                        DottedGetExpression: (dottedExpr) => {
430
                            if (isDottedGetExpression(dottedExpr.parent)) {
28✔
431
                                return;
5✔
432
                            }
433
                            followConstRef(dottedExpr, innerNamespace, chain);
23✔
434
                        }
435
                    }), { walkMode: WalkMode.visitExpressionsRecursive });
436
                }
437
            }
438
            chain.pop();
97✔
439
            fullyValidated.add(constStatement);
97✔
440
        };
441

442
        for (const [, link] of scope.getConstMap()) {
2,478✔
443
            walkConst(link.item, link.file, []);
97✔
444
        }
445
        scope.addDiagnostics(diagnostics);
2,478✔
446
    }
447

448
    /**
449
     * Validate every function call to `CreateObject`.
450
     * Ideally we would create better type checking/handling for this, but in the mean time, we know exactly
451
     * what these calls are supposed to look like, and this is a very common thing for brs devs to do, so just
452
     * do this manually for now.
453
     */
454
    protected validateCreateObjectCalls(file: BrsFile) {
455
        const diagnostics: BsDiagnostic[] = [];
2,514✔
456

457
        for (const call of file?.functionCalls ?? []) {
2,514!
458
            //skip non CreateObject function calls
459
            if (call.name?.toLowerCase() !== 'createobject' || !isLiteralExpression(call?.args[0]?.expression)) {
225!
460
                continue;
176✔
461
            }
462
            const firstParamToken = (call?.args[0]?.expression as any)?.token;
49!
463
            const firstParamStringValue = firstParamToken?.text?.replace(/"/g, '');
49!
464
            //if this is a `createObject('roSGNode'` call, only support known sg node types
465
            if (firstParamStringValue?.toLowerCase() === 'rosgnode' && isLiteralExpression(call?.args[1]?.expression)) {
49!
466
                const componentName: Token = (call?.args[1]?.expression as any)?.token;
16!
467
                //don't validate any components with a colon in their name (probably component libraries, but regular components can have them too).
468
                if (componentName?.text?.includes(':')) {
16!
469
                    continue;
3✔
470
                }
471
                //add diagnostic for unknown components
472
                const unquotedComponentName = componentName?.text?.replace(/"/g, '');
13!
473
                if (unquotedComponentName && !platformNodeNames.has(unquotedComponentName.toLowerCase()) && !this.event.program.getComponent(unquotedComponentName)) {
13✔
474
                    this.addDiagnosticOnce({
5✔
475
                        file: file as BscFile,
476
                        ...DiagnosticMessages.unknownRoSGNode(unquotedComponentName),
477
                        range: componentName.range
478
                    });
479
                } else if (call?.args.length !== 2) {
8!
480
                    // roSgNode should only ever have 2 args in `createObject`
481
                    this.addDiagnosticOnce({
1✔
482
                        file: file as BscFile,
483
                        ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, [2], call?.args.length),
3!
484
                        range: call.range
485
                    });
486
                }
487
            } else if (!platformComponentNames.has(firstParamStringValue.toLowerCase())) {
33✔
488
                this.addDiagnosticOnce({
9✔
489
                    file: file as BscFile,
490
                    ...DiagnosticMessages.unknownBrightScriptComponent(firstParamStringValue),
491
                    range: firstParamToken.range
492
                });
493
            } else {
494
                // This is valid brightscript component
495
                // Test for invalid arg counts
496
                const brightScriptComponent: BRSComponentData = components[firstParamStringValue.toLowerCase()];
24✔
497
                // Valid arg counts for createObject are 1+ number of args for constructor
498
                let validArgCounts = brightScriptComponent.constructors.map(cnstr => cnstr.params.length + 1);
27✔
499
                if (validArgCounts.length === 0) {
24✔
500
                    // no constructors for this component, so createObject only takes 1 arg
501
                    validArgCounts = [1];
4✔
502
                }
503
                if (!validArgCounts.includes(call?.args.length)) {
24!
504
                    // Incorrect number of arguments included in `createObject()`
505
                    this.addDiagnosticOnce({
5✔
506
                        file: file as BscFile,
507
                        ...DiagnosticMessages.mismatchCreateObjectArgumentCount(firstParamStringValue, validArgCounts, call?.args.length),
15!
508
                        range: call.range
509
                    });
510
                }
511

512
                // Test for deprecation
513
                if (brightScriptComponent.isDeprecated) {
24!
514
                    this.addDiagnosticOnce({
×
515
                        file: file as BscFile,
516
                        ...DiagnosticMessages.deprecatedBrightScriptComponent(firstParamStringValue, brightScriptComponent.deprecatedDescription),
517
                        range: call.range
518
                    });
519
                }
520
            }
521
        }
522
        this.event.scope.addDiagnostics(diagnostics);
2,514✔
523
    }
524

525
    /**
526
     * Adds a diagnostic to the first scope for this key. Prevents duplicate diagnostics
527
     * for diagnostics where scope isn't important. (i.e. CreateObject validations)
528
     */
529
    private addDiagnosticOnce(diagnostic: BsDiagnostic) {
530
        this.onceCache.getOrAdd(`${diagnostic.code}-${diagnostic.message}-${util.rangeToString(diagnostic.range)}`, () => {
20✔
531
            this.event.scope.addDiagnostics([diagnostic]);
16✔
532
            return true;
16✔
533
        });
534
    }
535
    private onceCache = new Cache<string, boolean>();
1,500✔
536

537
    private addDiagnostic(diagnostic: BsDiagnostic) {
538
        this.event.scope.addDiagnostics([diagnostic]);
116✔
539
    }
540

541
    /**
542
     * Add a diagnostic (to the first scope) that will have `relatedInformation` for each affected scope
543
     */
544
    private addMultiScopeDiagnostic(diagnostic: BsDiagnostic, message = 'Not defined in scope') {
116✔
545
        diagnostic = this.multiScopeCache.getOrAdd(`${diagnostic.file?.srcPath}-${diagnostic.code}-${diagnostic.message}-${util.rangeToString(diagnostic.range)}`, () => {
124!
546
            if (!diagnostic.relatedInformation) {
116✔
547
                diagnostic.relatedInformation = [];
108✔
548
            }
549
            this.addDiagnostic(diagnostic);
116✔
550
            return diagnostic;
116✔
551
        });
552
        const info = {
124✔
553
            message: `${message} '${this.event.scope.name}'`
554
        } as DiagnosticRelatedInformation;
555
        if (isXmlScope(this.event.scope) && this.event.scope.xmlFile?.srcPath) {
124!
556
            info.location = util.createLocation(
31✔
557
                URI.file(this.event.scope.xmlFile.srcPath).toString(),
558
                this.event.scope?.xmlFile?.ast?.component?.getAttribute('name')?.value.range ?? util.createRange(0, 0, 0, 10)
558!
559
            );
560
        } else {
561
            info.location = util.createLocation(
93✔
562
                URI.file(diagnostic.file.srcPath).toString(),
563
                diagnostic.range
564
            );
565
        }
566
        diagnostic.relatedInformation.push(info);
124✔
567
    }
568

569
    private multiScopeCache = new Cache<string, BsDiagnostic>();
1,500✔
570
}
571

572
interface ExpressionInfo {
573
    parts: Readonly<[VariableExpression, ...DottedGetExpression[]]>;
574
    expression: Readonly<Expression>;
575
    /**
576
     * The full namespace name that encloses this expression
577
     */
578
    enclosingNamespaceNameLower?: string;
579
}
580
type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> };
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc