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

rokucommunity / brighterscript / #15908

12 May 2026 07:29PM UTC coverage: 86.923% (+0.006%) from 86.917%
#15908

push

web-flow
Merge 39c1aae01 into ce68f5cb7

15646 of 19004 branches covered (82.33%)

Branch coverage included in aggregate %.

28 of 28 new or added lines in 8 files covered. (100.0%)

112 existing lines in 4 files now uncovered.

16359 of 17816 relevant lines covered (91.82%)

27323.13 hits per line

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

84.06
/src/CrossScopeValidator.ts
1
import type { UnresolvedSymbol } from './AstValidationSegmenter';
2
import type { Scope } from './Scope';
3
import type { BrsFile, ProvidedSymbol } from './files/BrsFile';
4
import { DiagnosticMessages } from './DiagnosticMessages';
1✔
5
import type { Program } from './Program';
6
import { util } from './util';
1✔
7
import { SymbolTypeFlag } from './SymbolTypeFlag';
1✔
8
import type { BscSymbol } from './SymbolTable';
9
import { isCallExpression, isConstStatement, isEnumStatement, isEnumType, isFunctionStatement, isInheritableType, isInterfaceStatement, isNamespaceStatement, isNamespaceType, isReferenceType, isTypedFunctionType, isUnionType } from './astUtils/reflection';
1✔
10
import type { ReferenceType } from './types/ReferenceType';
11
import { getAllRequiredSymbolNames } from './types/ReferenceType';
1✔
12
import type { TypeChainEntry, TypeChainProcessResult } from './interfaces';
13
import { BscTypeKind } from './types/BscTypeKind';
1✔
14
import { getAllTypesFromCompoundType } from './types/helpers';
1✔
15
import type { BscType } from './types/BscType';
16
import type { BscFile } from './files/BscFile';
17
import type { ClassStatement, ConstStatement, EnumMemberStatement, EnumStatement, InterfaceStatement, NamespaceStatement } from './parser/Statement';
18
import { ParseMode } from './parser/Parser';
1✔
19
import { globalFile } from './globalCallables';
1✔
20
import type { DottedGetExpression, VariableExpression } from './parser/Expression';
21
import type { InheritableType } from './types';
22

23

24
interface FileSymbolPair {
25
    file: BscFile;
26
    symbol: BscSymbol;
27
}
28

29
interface SymbolLookupKeys {
30
    potentialTypeKey: string;
31
    key: string;
32
    namespacedKey: string;
33
    namespacedPotentialTypeKey: string;
34
}
35

36
const CrossScopeValidatorDiagnosticTag = 'CrossScopeValidator';
1✔
37

38
export class ProvidedNode {
1✔
39

40
    namespaces = new Map<string, ProvidedNode>();
3,368✔
41
    symbols = new Map<string, FileSymbolPair>();
3,368✔
42

43
    constructor(public key: string = '', private componentsMap?: Map<string, FileSymbolPair>) { }
3,368!
44

45

46
    getSymbolByKey(symbolKeys: SymbolLookupKeys): FileSymbolPair {
47
        return this.getSymbol(symbolKeys.namespacedKey) ??
1,015✔
48
            this.getSymbol(symbolKeys.key) ??
1,015✔
49
            this.getSymbol(symbolKeys.namespacedPotentialTypeKey) ??
1,015✔
50
            this.getSymbol(symbolKeys.potentialTypeKey);
51
    }
52

53
    getSymbol(symbolName: string): FileSymbolPair {
54
        if (!symbolName) {
3,132✔
55
            return;
1,299✔
56
        }
57
        const lowerSymbolName = symbolName.toLowerCase();
1,833✔
58
        if (this.componentsMap?.has(lowerSymbolName)) {
1,833✔
59
            return this.componentsMap.get(lowerSymbolName);
6✔
60
        }
61
        let lowerSymbolNameParts = lowerSymbolName.split('.');
1,827✔
62
        return this.getSymbolByNameParts(lowerSymbolNameParts, this);
1,827✔
63
    }
64

65
    getNamespace(namespaceName: string): ProvidedNode {
66
        let lowerSymbolNameParts = namespaceName.toLowerCase().split('.');
5,633✔
67
        return this.getNamespaceByNameParts(lowerSymbolNameParts);
5,633✔
68
    }
69

70
    getSymbolByNameParts(lowerSymbolNameParts: string[], root: ProvidedNode): FileSymbolPair {
71
        const first = lowerSymbolNameParts?.[0];
3,314!
72
        const rest = lowerSymbolNameParts.slice(1);
3,314✔
73
        if (!first) {
3,314✔
74
            return;
149✔
75
        }
76
        if (this.symbols.has(first)) {
3,165✔
77
            let result = this.symbols.get(first);
821✔
78
            let currentType = result.symbol.type;
821✔
79

80
            for (const namePart of rest) {
821✔
81
                if (isTypedFunctionType(currentType)) {
63✔
82
                    const returnType = currentType.returnType;
7✔
83
                    if (returnType.isResolvable()) {
7✔
84
                        currentType = returnType;
4✔
85
                    } else if (isReferenceType(returnType)) {
3!
86
                        const fullName = returnType.fullName;
3✔
87
                        if (fullName.includes('.')) {
3!
88
                            currentType = root.getSymbol(fullName)?.symbol?.type;
×
89
                        } else {
90
                            currentType = this.getSymbol(fullName)?.symbol?.type ??
3!
91
                                root.getSymbol(fullName)?.symbol?.type;
×
92
                        }
93
                    }
94
                }
95
                let typesToTry = [currentType];
63✔
96
                if (isEnumType(currentType)) {
63✔
97
                    typesToTry.push(currentType.defaultMemberType);
39✔
98
                }
99
                if (isInheritableType(currentType)) {
63✔
100
                    let inheritableType = currentType;
15✔
101
                    while (inheritableType?.parentType) {
15!
102
                        let parentType = inheritableType.parentType as BscType;
×
103
                        if (isReferenceType(inheritableType.parentType)) {
×
104
                            const fullName = inheritableType.parentType.fullName;
×
105
                            if (fullName.includes('.')) {
×
106
                                parentType = root.getSymbol(fullName)?.symbol?.type;
×
107
                            } else {
108
                                parentType = this.getSymbol(fullName)?.symbol?.type ??
×
109
                                    root.getSymbol(fullName)?.symbol?.type;
×
110
                            }
111
                        }
112
                        typesToTry.push(parentType);
×
113
                        inheritableType = parentType as InheritableType;
×
114
                    }
115

116
                }
117
                const extraData = {};
63✔
118

119
                for (const curType of typesToTry) {
63✔
120
                    currentType = curType?.getMemberType(namePart, { flags: SymbolTypeFlag.runtime, data: extraData });
75!
121
                    if (isReferenceType(currentType)) {
75✔
122
                        const memberLookup = currentType.fullName;
6✔
123
                        currentType = this.getSymbol(memberLookup.toLowerCase())?.symbol?.type ?? root.getSymbol(memberLookup.toLowerCase())?.symbol?.type;
6!
124
                    }
125
                    if (currentType) {
75✔
126
                        break;
49✔
127
                    }
128
                }
129

130
                if (!currentType) {
63✔
131
                    return;
14✔
132
                }
133
                // get specific member
134
                result = {
49✔
135
                    ...result, symbol: { name: namePart, type: currentType, data: extraData, flags: SymbolTypeFlag.runtime }
136
                };
137
            }
138
            return result;
807✔
139

140
        } else if (rest && this.namespaces.has(first)) {
2,344✔
141
            const node = this.namespaces.get(first);
1,487✔
142
            const parts = node.getSymbolByNameParts(rest, root);
1,487✔
143

144
            return parts;
1,487✔
145
        }
146
    }
147

148
    getNamespaceByNameParts(lowerSymbolNameParts: string[]): ProvidedNode {
149
        const first = lowerSymbolNameParts?.[0]?.toLowerCase();
7,272!
150
        const rest = lowerSymbolNameParts.slice(1);
7,272✔
151
        if (!first) {
7,272✔
152
            return;
137✔
153
        }
154
        if (this.namespaces.has(first)) {
7,135✔
155
            const node = this.namespaces.get(first);
1,355✔
156
            const result = rest?.length > 0 ? node.getNamespaceByNameParts(rest) : node;
1,355!
157
            return result;
1,355✔
158
        }
159
    }
160

161
    addSymbol(symbolName: string, symbolPair: FileSymbolPair) {
162
        let lowerSymbolNameParts = symbolName.toLowerCase().split('.');
5,163✔
163
        return this.addSymbolByNameParts(lowerSymbolNameParts, symbolPair);
5,163✔
164
    }
165

166
    private addSymbolByNameParts(lowerSymbolNameParts: string[], symbolPair: FileSymbolPair) {
167
        const first = lowerSymbolNameParts?.[0];
7,270!
168
        const rest = lowerSymbolNameParts?.slice(1);
7,270!
169
        let isDuplicate = false;
7,270✔
170
        if (!first) {
7,270!
171
            return;
×
172
        }
173
        if (rest?.length > 0) {
7,270!
174
            // first must be a namespace
175
            let namespaceNode = this.namespaces.get(first);
2,107✔
176
            if (!namespaceNode) {
2,107✔
177
                namespaceNode = new ProvidedNode(first);
1,056✔
178
                this.namespaces.set(first, namespaceNode);
1,056✔
179
            }
180
            return namespaceNode.addSymbolByNameParts(rest, symbolPair);
2,107✔
181
        } else {
182
            if (this.namespaces.get(first)) {
5,163✔
183
                // trying to add a symbol that already exists as a namespace - this is a duplicate
184
                return true;
5✔
185
            }
186

187
            // just add it to the symbols
188
            const existingSymbolPair = this.symbols.get(first);
5,158✔
189
            if (!existingSymbolPair) {
5,158✔
190
                this.symbols.set(first, symbolPair);
4,525✔
191
            } else {
192
                isDuplicate = existingSymbolPair.symbol.data?.definingNode !== symbolPair.symbol.data?.definingNode;
633!
193
            }
194
        }
195
        return isDuplicate;
5,158✔
196
    }
197
}
198

199

200
export class CrossScopeValidator {
1✔
201

202
    constructor(public program: Program) { }
2,506✔
203

204
    private symbolMapKeys(symbol: UnresolvedSymbol): SymbolLookupKeys[] {
205
        let keysArray = new Array<SymbolLookupKeys>();
1,130✔
206
        let unnamespacedNameLowers: string[] = [];
1,130✔
207

208
        function joinTypeChainForKey(typeChain: TypeChainEntry[], firstType?: BscType) {
209
            firstType ||= typeChain[0].type;
1,130✔
210
            const unnamespacedNameLower = typeChain.map((tce, i) => {
1,130✔
211
                if (i === 0) {
2,090✔
212
                    if (isReferenceType(firstType)) {
1,130✔
213
                        return firstType.fullName;
773✔
214
                    } else if (isInheritableType(firstType)) {
357✔
215
                        return tce.type.toString();
6✔
216
                    }
217
                    return tce.name;
351✔
218
                }
219
                return tce.name;
960✔
220
            }).join('.').toLowerCase();
221
            return unnamespacedNameLower;
1,130✔
222
        }
223

224
        if (isUnionType(symbol.typeChain[0].type) && symbol.typeChain[0].data.isInstance) {
1,130!
225
            const allUnifiedTypes = getAllTypesFromCompoundType(symbol.typeChain[0].type);
×
226
            for (const unifiedType of allUnifiedTypes) {
×
227
                unnamespacedNameLowers.push(joinTypeChainForKey(symbol.typeChain, unifiedType));
×
228
            }
229

230
        } else {
231
            unnamespacedNameLowers.push(joinTypeChainForKey(symbol.typeChain));
1,130✔
232
        }
233

234
        for (const unnamespacedNameLower of unnamespacedNameLowers) {
1,130✔
235
            const lowerFirst = symbol.typeChain[0]?.name?.toLowerCase() ?? '';
1,130!
236
            let namespacedName = '';
1,130✔
237
            let lowerNamespacePrefix = '';
1,130✔
238
            let namespacedPotentialTypeKey = '';
1,130✔
239
            if (symbol.containingNamespaces?.length > 0 && symbol.typeChain[0]?.name.toLowerCase() !== symbol.containingNamespaces[0].toLowerCase()) {
1,130!
240
                lowerNamespacePrefix = `${(symbol.containingNamespaces ?? []).join('.')}`.toLowerCase();
63!
241
            }
242
            if (lowerNamespacePrefix) {
1,130✔
243
                namespacedName = `${lowerNamespacePrefix}.${unnamespacedNameLower}`;
63✔
244
                namespacedPotentialTypeKey = `${lowerNamespacePrefix}.${lowerFirst}`;
63✔
245
            }
246

247
            keysArray.push({
1,130✔
248
                potentialTypeKey: lowerFirst, // first entry in type chain (useful for enum types, typecasts, etc.)
249
                key: unnamespacedNameLower, //full name used in code (useful for namespaced symbols)
250
                namespacedKey: namespacedName, // full name including namespaces (useful for relative symbols in a namespace)
251
                namespacedPotentialTypeKey: namespacedPotentialTypeKey //first entry in chain, prefixed with current namespace
252
            });
253
        }
254
        return keysArray;
1,130✔
255
    }
256

257
    resolutionsMap = new Map<UnresolvedSymbol, Set<{ scope: Scope; sourceFile: BscFile; providedSymbol: BscSymbol }>>();
2,506✔
258
    providedTreeMap = new Map<string, { duplicatesMap: Map<string, Set<FileSymbolPair>>; providedTree: ProvidedNode }>();
2,506✔
259

260

261
    private componentsMap = new Map<string, FileSymbolPair>();
2,506✔
262

263
    getRequiredMap(scope: Scope) {
264
        const map = new Map<SymbolLookupKeys, UnresolvedSymbol>();
2,970✔
265
        scope.enumerateBrsFiles((file) => {
2,970✔
266
            for (const symbol of file.requiredSymbols) {
3,616✔
267
                const symbolKeysArray = this.symbolMapKeys(symbol);
1,130✔
268
                for (const symbolKeys of symbolKeysArray) {
1,130✔
269
                    map.set(symbolKeys, symbol);
1,130✔
270
                }
271
            }
272
        });
273
        return map;
2,970✔
274
    }
275

276
    getProvidedTree(scope: Scope) {
277
        if (this.providedTreeMap.has(scope.name)) {
3,234✔
278
            return this.providedTreeMap.get(scope.name);
922✔
279
        }
280
        const providedTree = new ProvidedNode('', this.componentsMap);
2,312✔
281
        let duplicatesMap: Map<string, Set<FileSymbolPair>> = null;
2,312✔
282

283
        const referenceTypesMap = new Map<{ symbolName: string; file: BscFile; symbolObj: ProvidedSymbol }, Array<{ name: string; namespacedName?: string }>>();
2,312✔
284

285

286
        const addSymbolWithDuplicates = (symbolName: string, file: BscFile, symbolObj: ProvidedSymbol) => {
2,312✔
287
            // eslint-disable-next-line no-bitwise
288
            const globalSymbol = this.program.globalScope.symbolTable.getSymbol(symbolName, SymbolTypeFlag.typetime | SymbolTypeFlag.runtime);
5,163✔
289
            const symbolIsNamespace = providedTree.getNamespace(symbolName);
5,163✔
290
            const isDupe = providedTree.addSymbol(symbolName, { file: file, symbol: symbolObj.symbol });
5,163✔
291
            if (symbolIsNamespace || globalSymbol || isDupe || symbolObj.duplicates.length > 0) {
5,163✔
292
                let dupesSet = duplicatesMap?.get(symbolName) ?? null;
56✔
293
                if (!dupesSet) {
56✔
294
                    dupesSet = new Set<{ file: BrsFile; symbol: BscSymbol }>();
46✔
295
                    if (!duplicatesMap) {
46✔
296
                        duplicatesMap = new Map<string, Set<FileSymbolPair>>();
43✔
297
                    }
298
                    duplicatesMap.set(symbolName, dupesSet);
46✔
299
                    const existing = providedTree.getSymbol(symbolName);
46✔
300
                    if (existing) {
46✔
301
                        dupesSet.add(existing);
45✔
302
                    }
303
                }
304
                if (!dupesSet.has({ file: file, symbol: symbolObj.symbol })) {
56!
305
                    dupesSet.add({ file: file, symbol: symbolObj.symbol });
56✔
306
                }
307
                if (symbolIsNamespace) {
56✔
308
                    const namespaceContainer = scope.getNamespace(symbolName);
5✔
309
                    const nsNode = namespaceContainer?.namespaceStatements?.[0];
5!
310
                    if (nsNode) {
5!
311
                        const nsFile = namespaceContainer.file;
5✔
312
                        const nsType = nsNode.getType({ flags: SymbolTypeFlag.typetime });
5✔
313
                        let nsSymbol: BscSymbol = {
5✔
314
                            name: nsNode.getName(ParseMode.BrighterScript),
315
                            type: nsType,
316
                            data: { definingNode: nsNode },
317
                            flags: SymbolTypeFlag.typetime
318
                        };
319
                        if (nsSymbol && !dupesSet.has({ file: nsFile, symbol: nsSymbol })) {
5!
320
                            dupesSet.add({ file: nsFile, symbol: nsSymbol });
5✔
321
                        }
322
                    }
323
                }
324
                for (const providedDupeSymbol of symbolObj.duplicates) {
56✔
325
                    if (!dupesSet.has({ file: file, symbol: providedDupeSymbol })) {
17!
326
                        dupesSet.add({ file: file, symbol: providedDupeSymbol });
17✔
327
                    }
328
                }
329
                if (globalSymbol) {
56✔
330
                    dupesSet.add({ file: globalFile, symbol: globalSymbol[0] });
15✔
331
                }
332
            }
333
        };
334

335
        scope.enumerateBrsFiles((file) => {
2,312✔
336
            for (const [_, nameMap] of file.providedSymbols.symbolMap.entries()) {
2,780✔
337

338
                for (const [symbolName, symbolObj] of nameMap.entries()) {
5,560✔
339
                    if (isNamespaceType(symbolObj.symbol.type)) {
5,107!
UNCOV
340
                        continue;
×
341
                    }
342
                    addSymbolWithDuplicates(symbolName, file, symbolObj);
5,107✔
343
                }
344
            }
345

346
            // find all "provided symbols" that are reference types
347
            for (const [_, nameMap] of file.providedSymbols.referenceSymbolMap.entries()) {
2,780✔
348
                for (const [symbolName, symbolObj] of nameMap.entries()) {
5,560✔
349
                    const symbolType = symbolObj.symbol.type;
78✔
350
                    const namespaceLower = symbolObj.symbol.data?.definingNode?.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript).toLowerCase();
78!
351
                    const allNames = getAllRequiredSymbolNames(symbolType, namespaceLower);
78✔
352

353
                    referenceTypesMap.set({ symbolName: symbolName, file: file, symbolObj: symbolObj }, allNames);
78✔
354
                }
355
            }
356
        });
357

358
        // check provided reference types to see if they exist yet!
359
        while (referenceTypesMap.size > 0) {
2,312✔
360
            let addedSymbol = false;
35✔
361
            for (const [refTypeDetails, neededNames] of referenceTypesMap.entries()) {
35✔
362
                let foundNames = 0;
81✔
363
                for (const neededName of neededNames) {
81✔
364
                    // check if name exists or namespaced version exists
365
                    if (providedTree.getSymbol(neededName.name) ?? providedTree.getSymbol(neededName.namespacedName)) {
77✔
366
                        foundNames++;
52✔
367
                    }
368
                }
369
                if (neededNames.length === foundNames) {
81✔
370
                    //found all that were needed
371
                    addSymbolWithDuplicates(refTypeDetails.symbolName, refTypeDetails.file, refTypeDetails.symbolObj);
56✔
372
                    referenceTypesMap.delete(refTypeDetails);
56✔
373
                    addedSymbol = true;
56✔
374
                }
375
            }
376
            if (!addedSymbol) {
35✔
377
                break;
10✔
378
            }
379
        }
380

381
        const result = { duplicatesMap: duplicatesMap, providedTree: providedTree };
2,312✔
382
        this.providedTreeMap.set(scope.name, result);
2,312✔
383
        return result;
2,312✔
384
    }
385

386
    getIssuesForScope(scope: Scope) {
387
        const requiredMap = this.getRequiredMap(scope);
2,970✔
388
        const { providedTree, duplicatesMap } = this.getProvidedTree(scope);
2,970✔
389

390
        const missingSymbols = new Set<UnresolvedSymbol>();
2,970✔
391

392
        for (const [symbolKeys, unresolvedSymbol] of requiredMap.entries()) {
2,970✔
393

394
            // check global scope for components
395
            if (unresolvedSymbol.typeChain.length === 1 && this.program.globalScope.symbolTable.getSymbol(unresolvedSymbol.typeChain[0].name, unresolvedSymbol.flags)) {
1,130✔
396
                //symbol is available in global scope. ignore it
397
                continue;
115✔
398
            }
399
            let foundSymbol = providedTree.getSymbolByKey(symbolKeys);
1,015✔
400

401
            if (foundSymbol) {
1,015✔
402
                if (!unresolvedSymbol.typeChain[0].data?.isInstance) {
699!
403
                    let resolvedListForSymbol = this.resolutionsMap.get(unresolvedSymbol);
687✔
404
                    if (!resolvedListForSymbol) {
687✔
405
                        resolvedListForSymbol = new Set<{ scope: Scope; sourceFile: BrsFile; providedSymbol: BscSymbol }>();
411✔
406
                        this.resolutionsMap.set(unresolvedSymbol, resolvedListForSymbol);
411✔
407
                    }
408
                    resolvedListForSymbol.add({
687✔
409
                        scope: scope,
410
                        sourceFile: foundSymbol.file,
411
                        providedSymbol: foundSymbol.symbol
412
                    });
413
                }
414
            } else {
415
                let foundNamespace = providedTree.getNamespace(symbolKeys.key);
316✔
416

417
                if (foundNamespace) {
316✔
418
                    // this symbol turned out to be a namespace. This is allowed for alias statements
419
                    // TODO: add check to make sure this usage is from an alias statement
420
                } else {
421
                    // did not find symbol!
422
                    const missing = { ...unresolvedSymbol };
310✔
423
                    let namespaceNode = providedTree;
310✔
424
                    let currentKnownType;
425
                    for (const chainEntry of missing.typeChain) {
310✔
426
                        if (!chainEntry.isResolved) {
555✔
427
                            // for each unresolved part of a chain, see if we can resolve it with stuff from the provided tree
428
                            // and if so, mark it as resolved
429
                            const lookupName = (chainEntry.type as ReferenceType)?.fullName ?? chainEntry.name;
328!
430
                            if (!currentKnownType) {
328!
431
                                namespaceNode = namespaceNode?.getNamespaceByNameParts([chainEntry.name]);
328!
432

433
                            }
434
                            if (namespaceNode) {
328✔
435
                                chainEntry.isResolved = true;
18✔
436
                            } else {
437
                                if (currentKnownType) {
310!
UNCOV
438
                                    currentKnownType = currentKnownType.getMemberType(chainEntry.name, { flags: SymbolTypeFlag.runtime });
×
439
                                } else {
440
                                    currentKnownType = providedTree.getSymbol(lookupName.toLowerCase())?.symbol?.type;
310!
441
                                }
442

443
                                if (currentKnownType?.isResolvable()) {
310!
UNCOV
444
                                    chainEntry.isResolved = true;
×
445
                                } else {
446
                                    break;
310✔
447
                                }
448
                            }
449
                        }
450
                    }
451
                    missingSymbols.add(unresolvedSymbol);
310✔
452
                }
453
            }
454
        }
455
        return { missingSymbols: missingSymbols, duplicatesMap: duplicatesMap };
2,970✔
456
    }
457

458
    clearResolutionsForFile(file: BrsFile) {
459
        for (const symbol of this.resolutionsMap.keys()) {
225✔
460
            if (symbol.file === file) {
168✔
461
                this.resolutionsMap.delete(symbol);
7✔
462
            }
463
        }
464
    }
465

466
    clearResolutionsForScopes(scopes: Scope[]) {
467
        const lowerScopeNames = new Set(scopes.map(scope => scope.name.toLowerCase()));
2,958✔
468
        for (const [symbol, resolutionInfos] of this.resolutionsMap.entries()) {
2,120✔
469
            for (const info of resolutionInfos.values()) {
158✔
470
                if (lowerScopeNames.has(info.scope.name.toLowerCase())) {
315✔
471
                    resolutionInfos.delete(info);
279✔
472
                }
473
            }
474
            if (resolutionInfos.size === 0) {
158✔
475
                this.resolutionsMap.delete(symbol);
143✔
476
            }
477
        }
478
    }
479

480
    getFilesRequiringChangedSymbol(scopes: Scope[], changedSymbols: Map<SymbolTypeFlag, Set<string>>) {
481
        const filesThatNeedRevalidation = new Set<BscFile>();
2,120✔
482
        const filesThatDoNotNeedRevalidation = new Set<BscFile>();
2,120✔
483

484
        for (const scope of scopes) {
2,120✔
485
            scope.enumerateBrsFiles((file) => {
2,958✔
486
                if (filesThatNeedRevalidation.has(file) || filesThatDoNotNeedRevalidation.has(file)) {
3,592✔
487
                    return;
1,221✔
488
                }
489
                if (util.hasAnyRequiredSymbolChanged(file.requiredSymbols, changedSymbols)) {
2,371✔
490
                    filesThatNeedRevalidation.add(file);
518✔
491
                    return;
518✔
492
                }
493
                filesThatDoNotNeedRevalidation.add(file);
1,853✔
494
            });
495
        }
496
        return filesThatNeedRevalidation;
2,120✔
497
    }
498

499
    getScopesRequiringChangedSymbol(scopes: Scope[], changedSymbols: Map<SymbolTypeFlag, Set<string>>) {
UNCOV
500
        const scopesThatNeedRevalidation = new Set<Scope>();
×
UNCOV
501
        const filesAlreadyChecked = new Set<BrsFile>();
×
502

503
        for (const scope of scopes) {
×
504
            scope.enumerateBrsFiles((file) => {
×
UNCOV
505
                if (filesAlreadyChecked.has(file) || scopesThatNeedRevalidation.has(scope)) {
×
506
                    return;
×
507
                }
508
                filesAlreadyChecked.add(file);
×
509

UNCOV
510
                if (util.hasAnyRequiredSymbolChanged(file.requiredSymbols, changedSymbols)) {
×
511
                    scopesThatNeedRevalidation.add(scope);
×
512
                }
513
            });
514
        }
UNCOV
515
        return scopesThatNeedRevalidation;
×
516
    }
517

518
    buildComponentsMap() {
519
        this.componentsMap.clear();
2,120✔
520
        // Add custom components
521
        for (let componentName of this.program.getSortedComponentNames()) {
2,120✔
522
            const typeName = 'rosgnode' + componentName;
677✔
523
            const component = this.program.getComponent(componentName);
677✔
524
            const componentSymbol = this.program.globalScope.symbolTable.getSymbol(typeName, SymbolTypeFlag.typetime)?.[0];
677✔
525
            if (componentSymbol && component) {
677✔
526
                this.componentsMap.set(typeName, { file: component.file, symbol: componentSymbol });
650✔
527
            }
528
        }
529
    }
530

531
    addDiagnosticsForScopes(scopes: Scope[]) { //, changedFiles: BrsFile[]) {
532
        const addDuplicateSymbolDiagnostics = true;
2,120✔
533
        const missingSymbolInScope = new Map<UnresolvedSymbol, Set<Scope>>();
2,120✔
534
        this.providedTreeMap.clear();
2,120✔
535
        this.clearResolutionsForScopes(scopes);
2,120✔
536

537
        // Check scope for duplicates and missing symbols
538
        for (const scope of scopes) {
2,120✔
539
            this.program.diagnostics.clearByFilter({
2,958✔
540
                scope: scope,
541
                tag: CrossScopeValidatorDiagnosticTag
542
            });
543

544
            const { missingSymbols, duplicatesMap } = this.getIssuesForScope(scope);
2,958✔
545
            if (addDuplicateSymbolDiagnostics && duplicatesMap) {
2,958✔
546
                for (const [_flag, dupeSet] of duplicatesMap.entries()) {
52✔
547
                    if (dupeSet.size > 1) {
55!
548

549
                        const dupesArray = [...dupeSet.values()];
55✔
550

551
                        for (let i = 0; i < dupesArray.length; i++) {
55✔
552
                            const dupe = dupesArray[i];
156✔
553

554
                            const dupeNode = dupe?.symbol?.data?.definingNode;
156!
555
                            if (!dupeNode) {
156✔
556
                                continue;
15✔
557
                            }
558
                            let thisName = dupe.symbol?.name;
141!
559
                            const wrappingNameSpace = dupeNode?.findAncestor<NamespaceStatement>(isNamespaceStatement);
141!
560

561
                            if (wrappingNameSpace) {
141✔
562
                                thisName = `${wrappingNameSpace.getName(ParseMode.BrighterScript)}.` + thisName;
31✔
563
                            }
564

565
                            const thisNodeKindName = util.getAstNodeFriendlyName(dupeNode) ?? 'Item';
141!
566

567
                            for (let j = 0; j < dupesArray.length; j++) {
141✔
568
                                if (i === j) {
435✔
569
                                    continue;
141✔
570
                                }
571
                                const otherDupe = dupesArray[j];
294✔
572
                                if (!otherDupe || dupe.symbol === otherDupe.symbol) {
294✔
573
                                    continue;
98✔
574
                                }
575

576
                                const otherDupeNode = otherDupe.symbol.data?.definingNode;
196!
577
                                const otherIsGlobal = otherDupe.file.srcPath === 'global';
196✔
578

579
                                if (isFunctionStatement(dupeNode) && isFunctionStatement(otherDupeNode)) {
196✔
580
                                    // duplicate functions are handled in ScopeValidator
581
                                    continue;
34✔
582
                                }
583
                                if (otherIsGlobal &&
162✔
584
                                    (isInterfaceStatement(dupeNode) ||
585
                                        isEnumStatement(dupeNode) ||
586
                                        isConstStatement(dupeNode))) {
587
                                    // these are allowed to shadow global functions
588
                                    continue;
14✔
589
                                }
590
                                let thatName = otherDupe.symbol?.name;
148!
591

592
                                if (otherDupeNode) {
148✔
593
                                    const otherWrappingNameSpace = otherDupeNode?.findAncestor<NamespaceStatement>(isNamespaceStatement);
126!
594
                                    if (otherWrappingNameSpace) {
126✔
595
                                        thatName = `${otherWrappingNameSpace.getName(ParseMode.BrighterScript)}.` + thatName;
42✔
596
                                    }
597
                                }
598

599
                                type AstNodeWithName = VariableExpression | DottedGetExpression | EnumStatement | ClassStatement | ConstStatement | EnumMemberStatement | InterfaceStatement;
600

601
                                const thatNodeKindName = otherIsGlobal ? 'Global Function' : util.getAstNodeFriendlyName(otherDupeNode) ?? 'Item';
148!
602
                                let thisNameRange = (dupeNode as AstNodeWithName)?.tokens?.name?.location?.range ?? dupeNode.location?.range;
148!
603
                                let thatNameRange = (otherDupeNode as AstNodeWithName)?.tokens?.name?.location?.range ?? otherDupeNode?.location?.range;
148✔
604

605
                                const relatedInformation = thatNameRange ? [{
148✔
606
                                    message: `${thatNodeKindName} declared here`,
607
                                    location: util.createLocationFromFileRange(otherDupe.file, thatNameRange)
608
                                }] : undefined;
609
                                this.program.diagnostics.register({
148✔
610
                                    ...DiagnosticMessages.nameCollision(thisNodeKindName, thatNodeKindName, thatName),
611
                                    location: util.createLocationFromFileRange(dupe.file, thisNameRange),
612
                                    relatedInformation: relatedInformation
613
                                }, {
614
                                    scope: scope,
615
                                    tags: [CrossScopeValidatorDiagnosticTag]
616
                                });
617
                            }
618
                        }
619
                    }
620
                }
621
            }
622
            // build map of the symbols and scopes where the symbols are missing per file
623
            for (const missingSymbol of missingSymbols) {
2,958✔
624

625
                let scopesWithMissingSymbol = missingSymbolInScope.get(missingSymbol);
306✔
626
                if (!scopesWithMissingSymbol) {
306✔
627
                    scopesWithMissingSymbol = new Set<Scope>();
259✔
628
                    missingSymbolInScope.set(missingSymbol, scopesWithMissingSymbol);
259✔
629
                }
630
                scopesWithMissingSymbol.add(scope);
306✔
631
            }
632
        }
633

634
        // If symbols are missing in SOME scopes, add diagnostic
635
        for (const [symbol, scopeList] of missingSymbolInScope.entries()) {
2,120✔
636
            const typeChainResult = util.processTypeChain(symbol.typeChain);
259✔
637

638
            //roku built-in type names (rosgnode*, etc.) aren't tracked in any symbol table;
639
            //skip cannot-find-name when the symbol's name is one of those built-ins.
640
            if (typeChainResult.itemName && util.isBuiltInType(typeChainResult.itemName)) {
259✔
641
                continue;
1✔
642
            }
643

644
            for (const scope of scopeList) {
258✔
645
                this.program.diagnostics.register({
261✔
646
                    ...this.getCannotFindDiagnostic(scope, symbol, typeChainResult),
647
                    location: typeChainResult.location
648
                }, {
649
                    scope: scope,
650
                    tags: [CrossScopeValidatorDiagnosticTag]
651
                });
652
            }
653
        }
654

655
        for (const resolution of this.getIncompatibleSymbolResolutions()) {
2,120✔
656
            const symbol = resolution.symbol;
6✔
657
            const incompatibleScopes = resolution.incompatibleScopes;
6✔
658
            if (incompatibleScopes.size > 1) {
6!
659
                const typeChainResult = util.processTypeChain(symbol.typeChain);
6✔
660
                const scopeList = [...incompatibleScopes.values()].map(s => s.name);
12✔
661
                this.program.diagnostics.register({
6✔
662
                    ...DiagnosticMessages.incompatibleSymbolDefinition(typeChainResult.fullChainName, { scopes: scopeList }),
663
                    location: typeChainResult.location
664
                }, {
665
                    tags: [CrossScopeValidatorDiagnosticTag]
666
                });
667
            }
668
        }
669
    }
670

671
    getIncompatibleSymbolResolutions() {
672
        const incompatibleResolutions = new Array<{ symbol: UnresolvedSymbol; incompatibleScopes: Set<Scope> }>();
2,121✔
673
        // check all resolutions and check if there are resolutions that are not compatible across scopes
674
        for (const [symbol, resolutionDetails] of this.resolutionsMap.entries()) {
2,121✔
675
            if (resolutionDetails.size < 2) {
428✔
676
                // there is only one resolution... no worries
677
                continue;
201✔
678
            }
679
            const resolutionsList = [...resolutionDetails];
227✔
680
            const prime = resolutionsList[0];
227✔
681
            let incompatibleScopes = new Set<Scope>();
227✔
682
            let addedPrime = false;
227✔
683
            for (let i = 1; i < resolutionsList.length; i++) {
227✔
684
                let providedSymbolType = prime.providedSymbol.type;
292✔
685
                const symbolInThisScope = resolutionsList[i].providedSymbol;
292✔
686

687
                //get more general type
688
                if (providedSymbolType.isEqual(symbolInThisScope.type)) {
292✔
689
                    //type in this scope is the same as one we're already checking
690
                } else if (providedSymbolType.isTypeCompatible(symbolInThisScope.type)) {
13✔
691
                    //type in this scope is compatible with one we're storing. use most generic
692
                    providedSymbolType = symbolInThisScope.type;
1✔
693
                } else if (symbolInThisScope.type.isTypeCompatible(providedSymbolType)) {
12!
694
                    // type we're storing is more generic that the type in this scope
695
                } else {
696
                    // type in this scope is not compatible with other types for this symbol
697
                    if (!addedPrime) {
12✔
698
                        incompatibleScopes.add(prime.scope);
7✔
699
                        addedPrime = true;
7✔
700
                    }
701
                    incompatibleScopes.add(resolutionsList[i].scope);
12✔
702
                }
703
            }
704

705
            if (incompatibleScopes.size > 1) {
227✔
706
                incompatibleResolutions.push({
7✔
707
                    symbol: symbol,
708
                    incompatibleScopes: incompatibleScopes
709
                });
710
            }
711
        }
712
        return incompatibleResolutions;
2,121✔
713
    }
714

715
    private getCannotFindDiagnostic(scope: Scope, unresolvedSymbol: UnresolvedSymbol, typeChainResult: TypeChainProcessResult) {
716
        const parentDescriptor = this.getParentTypeDescriptor(this.getProvidedTree(scope)?.providedTree, typeChainResult);
261!
717
        const symbolType = typeChainResult.astNode?.getType({ flags: unresolvedSymbol.flags });
261!
718
        if (isReferenceType(symbolType)) {
261✔
719
            const circularReferenceInfo = symbolType.getCircularReferenceInfo();
250✔
720
            if (circularReferenceInfo.isCircularReference) {
250✔
721
                let diagnosticDetail = util.getCircularReferenceDiagnosticDetail(circularReferenceInfo, typeChainResult.fullNameOfItem);
9✔
722
                return DiagnosticMessages.circularReferenceDetected(diagnosticDetail);
9✔
723
            }
724
        }
725

726
        if (isCallExpression(typeChainResult.astNode?.parent) && typeChainResult.astNode?.parent.callee === typeChainResult.astNode) {
252!
727
            return DiagnosticMessages.cannotFindFunction(typeChainResult.itemName, typeChainResult.fullNameOfItem, typeChainResult.itemParentTypeName, parentDescriptor);
53✔
728
        }
729
        return DiagnosticMessages.cannotFindName(typeChainResult.itemName, typeChainResult.fullNameOfItem, typeChainResult.itemParentTypeName, parentDescriptor);
199✔
730
    }
731

732
    private getParentTypeDescriptor(provided: ProvidedNode, typeChainResult: TypeChainProcessResult) {
733
        if (typeChainResult.itemParentTypeKind === BscTypeKind.NamespaceType || provided?.getNamespace(typeChainResult.itemParentTypeName)) {
261!
734
            return 'namespace';
122✔
735
        }
736
        return 'type';
139✔
737
    }
738

739
}
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