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

rokucommunity / brighterscript / #15035

15 Dec 2025 08:42PM UTC coverage: 86.889%. Remained the same
#15035

push

web-flow
Merge a60226157 into 2ea4d2108

14466 of 17575 branches covered (82.31%)

Branch coverage included in aggregate %.

113 of 217 new or added lines in 8 files covered. (52.07%)

116 existing lines in 6 files now uncovered.

15185 of 16550 relevant lines covered (91.75%)

24075.2 hits per line

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

83.59
/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 { getAllTypesFromComplexType } 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>();
2,929✔
41
    symbols = new Map<string, FileSymbolPair>();
2,929✔
42

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

45

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

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

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

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

80
            for (const namePart of rest) {
723✔
81
                if (isTypedFunctionType(currentType)) {
59✔
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];
59✔
96
                if (isEnumType(currentType)) {
59✔
97
                    typesToTry.push(currentType.defaultMemberType);
35✔
98
                }
99
                if (isInheritableType(currentType)) {
59✔
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 = {};
59✔
118

119
                for (const curType of typesToTry) {
59✔
120
                    currentType = curType?.getMemberType(namePart, { flags: SymbolTypeFlag.runtime, data: extraData });
67!
121
                    if (isReferenceType(currentType)) {
67✔
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) {
67✔
126
                        break;
49✔
127
                    }
128
                }
129

130
                if (!currentType) {
59✔
131
                    return;
10✔
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;
713✔
139

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

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

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

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

166
    private addSymbolByNameParts(lowerSymbolNameParts: string[], symbolPair: FileSymbolPair) {
167
        const first = lowerSymbolNameParts?.[0];
6,451!
168
        const rest = lowerSymbolNameParts?.slice(1);
6,451!
169
        let isDuplicate = false;
6,451✔
170
        if (!first) {
6,451!
171
            return;
×
172
        }
173
        if (rest?.length > 0) {
6,451!
174
            // first must be a namespace
175
            let namespaceNode = this.namespaces.get(first);
1,979✔
176
            if (!namespaceNode) {
1,979✔
177
                namespaceNode = new ProvidedNode(first);
988✔
178
                this.namespaces.set(first, namespaceNode);
988✔
179
            }
180
            return namespaceNode.addSymbolByNameParts(rest, symbolPair);
1,979✔
181
        } else {
182
            if (this.namespaces.get(first)) {
4,472✔
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);
4,467✔
189
            if (!existingSymbolPair) {
4,467✔
190
                this.symbols.set(first, symbolPair);
3,892✔
191
            } else {
192
                isDuplicate = existingSymbolPair.symbol.data?.definingNode !== symbolPair.symbol.data?.definingNode;
575!
193
            }
194
        }
195
        return isDuplicate;
4,467✔
196
    }
197
}
198

199

200
export class CrossScopeValidator {
1✔
201

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

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

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

224
        if (isUnionType(symbol.typeChain[0].type) && symbol.typeChain[0].data.isInstance) {
984!
NEW
225
            const allUnifiedTypes = getAllTypesFromComplexType(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));
984✔
232
        }
233

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

247
            keysArray.push({
984✔
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;
984✔
255
    }
256

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

260

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

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

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

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

285

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

332
        scope.enumerateBrsFiles((file) => {
1,941✔
333
            for (const [_, nameMap] of file.providedSymbols.symbolMap.entries()) {
2,341✔
334

335
                for (const [symbolName, symbolObj] of nameMap.entries()) {
4,682✔
336
                    if (isNamespaceType(symbolObj.symbol.type)) {
4,416!
337
                        continue;
×
338
                    }
339
                    addSymbolWithDuplicates(symbolName, file, symbolObj);
4,416✔
340
                }
341
            }
342

343
            // find all "provided symbols" that are reference types
344
            for (const [_, nameMap] of file.providedSymbols.referenceSymbolMap.entries()) {
2,341✔
345
                for (const [symbolName, symbolObj] of nameMap.entries()) {
4,682✔
346
                    const symbolType = symbolObj.symbol.type;
76✔
347
                    const namespaceLower = symbolObj.symbol.data?.definingNode?.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(ParseMode.BrighterScript).toLowerCase();
76!
348
                    const allNames = getAllRequiredSymbolNames(symbolType, namespaceLower);
76✔
349

350
                    referenceTypesMap.set({ symbolName: symbolName, file: file, symbolObj: symbolObj }, allNames);
76✔
351
                }
352
            }
353
        });
354

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

378
        const result = { duplicatesMap: duplicatesMap, providedTree: providedTree };
1,941✔
379
        this.providedTreeMap.set(scope.name, result);
1,941✔
380
        return result;
1,941✔
381
    }
382

383
    getIssuesForScope(scope: Scope) {
384
        const requiredMap = this.getRequiredMap(scope);
2,492✔
385
        const { providedTree, duplicatesMap } = this.getProvidedTree(scope);
2,492✔
386

387
        const missingSymbols = new Set<UnresolvedSymbol>();
2,492✔
388

389
        for (const [symbolKeys, unresolvedSymbol] of requiredMap.entries()) {
2,492✔
390

391
            // check global scope for components
392
            if (unresolvedSymbol.typeChain.length === 1 && this.program.globalScope.symbolTable.getSymbol(unresolvedSymbol.typeChain[0].name, unresolvedSymbol.flags)) {
984✔
393
                //symbol is available in global scope. ignore it
394
                continue;
107✔
395
            }
396
            let foundSymbol = providedTree.getSymbolByKey(symbolKeys);
877✔
397

398
            if (foundSymbol) {
877✔
399
                if (!unresolvedSymbol.typeChain[0].data?.isInstance) {
606!
400
                    let resolvedListForSymbol = this.resolutionsMap.get(unresolvedSymbol);
594✔
401
                    if (!resolvedListForSymbol) {
594✔
402
                        resolvedListForSymbol = new Set<{ scope: Scope; sourceFile: BrsFile; providedSymbol: BscSymbol }>();
354✔
403
                        this.resolutionsMap.set(unresolvedSymbol, resolvedListForSymbol);
354✔
404
                    }
405
                    resolvedListForSymbol.add({
594✔
406
                        scope: scope,
407
                        sourceFile: foundSymbol.file,
408
                        providedSymbol: foundSymbol.symbol
409
                    });
410
                }
411
            } else {
412
                let foundNamespace = providedTree.getNamespace(symbolKeys.key);
271✔
413

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

430
                            }
431
                            if (namespaceNode) {
281✔
432
                                chainEntry.isResolved = true;
16✔
433
                            } else {
434
                                if (currentKnownType) {
265!
435
                                    currentKnownType = currentKnownType.getMemberType(chainEntry.name, { flags: SymbolTypeFlag.runtime });
×
436
                                } else {
437
                                    currentKnownType = providedTree.getSymbol(lookupName.toLowerCase())?.symbol?.type;
265!
438
                                }
439

440
                                if (currentKnownType?.isResolvable()) {
265!
441
                                    chainEntry.isResolved = true;
×
442
                                } else {
443
                                    break;
265✔
444
                                }
445
                            }
446
                        }
447
                    }
448
                    missingSymbols.add(unresolvedSymbol);
265✔
449
                }
450
            }
451
        }
452
        return { missingSymbols: missingSymbols, duplicatesMap: duplicatesMap };
2,492✔
453
    }
454

455
    clearResolutionsForFile(file: BrsFile) {
456
        for (const symbol of this.resolutionsMap.keys()) {
199✔
457
            if (symbol.file === file) {
139✔
458
                this.resolutionsMap.delete(symbol);
7✔
459
            }
460
        }
461
    }
462

463
    clearResolutionsForScopes(scopes: Scope[]) {
464
        const lowerScopeNames = new Set(scopes.map(scope => scope.name.toLowerCase()));
2,486✔
465
        for (const [symbol, resolutionInfos] of this.resolutionsMap.entries()) {
1,678✔
466
            for (const info of resolutionInfos.values()) {
139✔
467
                if (lowerScopeNames.has(info.scope.name.toLowerCase())) {
281✔
468
                    resolutionInfos.delete(info);
249✔
469
                }
470
            }
471
            if (resolutionInfos.size === 0) {
139✔
472
                this.resolutionsMap.delete(symbol);
126✔
473
            }
474
        }
475
    }
476

477
    getFilesRequiringChangedSymbol(scopes: Scope[], changedSymbols: Map<SymbolTypeFlag, Set<string>>) {
478
        const filesThatNeedRevalidation = new Set<BscFile>();
1,678✔
479
        const filesThatDoNotNeedRevalidation = new Set<BscFile>();
1,678✔
480

481
        for (const scope of scopes) {
1,678✔
482
            scope.enumerateBrsFiles((file) => {
2,486✔
483
                if (filesThatNeedRevalidation.has(file) || filesThatDoNotNeedRevalidation.has(file)) {
3,027✔
484
                    return;
1,019✔
485
                }
486
                if (util.hasAnyRequiredSymbolChanged(file.requiredSymbols, changedSymbols)) {
2,008✔
487
                    filesThatNeedRevalidation.add(file);
444✔
488
                    return;
444✔
489
                }
490
                filesThatDoNotNeedRevalidation.add(file);
1,564✔
491
            });
492
        }
493
        return filesThatNeedRevalidation;
1,678✔
494
    }
495

496
    getScopesRequiringChangedSymbol(scopes: Scope[], changedSymbols: Map<SymbolTypeFlag, Set<string>>) {
497
        const scopesThatNeedRevalidation = new Set<Scope>();
×
498
        const filesAlreadyChecked = new Set<BrsFile>();
×
499

500
        for (const scope of scopes) {
×
501
            scope.enumerateBrsFiles((file) => {
×
502
                if (filesAlreadyChecked.has(file) || scopesThatNeedRevalidation.has(scope)) {
×
503
                    return;
×
504
                }
505
                filesAlreadyChecked.add(file);
×
506

507
                if (util.hasAnyRequiredSymbolChanged(file.requiredSymbols, changedSymbols)) {
×
508
                    scopesThatNeedRevalidation.add(scope);
×
509
                }
510
            });
511
        }
512
        return scopesThatNeedRevalidation;
×
513
    }
514

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

528
    addDiagnosticsForScopes(scopes: Scope[]) { //, changedFiles: BrsFile[]) {
529
        const addDuplicateSymbolDiagnostics = true;
1,678✔
530
        const missingSymbolInScope = new Map<UnresolvedSymbol, Set<Scope>>();
1,678✔
531
        this.providedTreeMap.clear();
1,678✔
532
        this.clearResolutionsForScopes(scopes);
1,678✔
533

534
        // Check scope for duplicates and missing symbols
535
        for (const scope of scopes) {
1,678✔
536
            this.program.diagnostics.clearByFilter({
2,486✔
537
                scope: scope,
538
                tag: CrossScopeValidatorDiagnosticTag
539
            });
540

541
            const { missingSymbols, duplicatesMap } = this.getIssuesForScope(scope);
2,486✔
542
            if (addDuplicateSymbolDiagnostics) {
2,486!
543
                for (const [_flag, dupeSet] of duplicatesMap.entries()) {
2,486✔
544
                    if (dupeSet.size > 1) {
53!
545

546
                        const dupesArray = [...dupeSet.values()];
53✔
547

548
                        for (let i = 0; i < dupesArray.length; i++) {
53✔
549
                            const dupe = dupesArray[i];
152✔
550

551
                            const dupeNode = dupe?.symbol?.data?.definingNode;
152!
552
                            if (!dupeNode) {
152✔
553
                                continue;
15✔
554
                            }
555
                            let thisName = dupe.symbol?.name;
137!
556
                            const wrappingNameSpace = dupeNode?.findAncestor<NamespaceStatement>(isNamespaceStatement);
137!
557

558
                            if (wrappingNameSpace) {
137✔
559
                                thisName = `${wrappingNameSpace.getName(ParseMode.BrighterScript)}.` + thisName;
31✔
560
                            }
561

562
                            const thisNodeKindName = util.getAstNodeFriendlyName(dupeNode) ?? 'Item';
137!
563

564
                            for (let j = 0; j < dupesArray.length; j++) {
137✔
565
                                if (i === j) {
427✔
566
                                    continue;
137✔
567
                                }
568
                                const otherDupe = dupesArray[j];
290✔
569
                                if (!otherDupe || dupe.symbol === otherDupe.symbol) {
290✔
570
                                    continue;
98✔
571
                                }
572

573
                                const otherDupeNode = otherDupe.symbol.data?.definingNode;
192!
574
                                const otherIsGlobal = otherDupe.file.srcPath === 'global';
192✔
575

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

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

596
                                type AstNodeWithName = VariableExpression | DottedGetExpression | EnumStatement | ClassStatement | ConstStatement | EnumMemberStatement | InterfaceStatement;
597

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

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

622
                let scopesWithMissingSymbol = missingSymbolInScope.get(missingSymbol);
265✔
623
                if (!scopesWithMissingSymbol) {
265✔
624
                    scopesWithMissingSymbol = new Set<Scope>();
232✔
625
                    missingSymbolInScope.set(missingSymbol, scopesWithMissingSymbol);
232✔
626
                }
627
                scopesWithMissingSymbol.add(scope);
265✔
628
            }
629
        }
630

631
        // If symbols are missing in SOME scopes, add diagnostic
632
        for (const [symbol, scopeList] of missingSymbolInScope.entries()) {
1,678✔
633
            const typeChainResult = util.processTypeChain(symbol.typeChain);
232✔
634

635
            for (const scope of scopeList) {
232✔
636
                this.program.diagnostics.register({
235✔
637
                    ...this.getCannotFindDiagnostic(scope, symbol, typeChainResult),
638
                    location: typeChainResult.location
639
                }, {
640
                    scope: scope,
641
                    tags: [CrossScopeValidatorDiagnosticTag]
642
                });
643
            }
644
        }
645

646
        for (const resolution of this.getIncompatibleSymbolResolutions()) {
1,678✔
647
            const symbol = resolution.symbol;
6✔
648
            const incompatibleScopes = resolution.incompatibleScopes;
6✔
649
            if (incompatibleScopes.size > 1) {
6!
650
                const typeChainResult = util.processTypeChain(symbol.typeChain);
6✔
651
                const scopeList = [...incompatibleScopes.values()].map(s => s.name);
12✔
652
                this.program.diagnostics.register({
6✔
653
                    ...DiagnosticMessages.incompatibleSymbolDefinition(typeChainResult.fullChainName, { scopes: scopeList }),
654
                    location: typeChainResult.location
655
                }, {
656
                    tags: [CrossScopeValidatorDiagnosticTag]
657
                });
658
            }
659
        }
660
    }
661

662
    getIncompatibleSymbolResolutions() {
663
        const incompatibleResolutions = new Array<{ symbol: UnresolvedSymbol; incompatibleScopes: Set<Scope> }>();
1,679✔
664
        // check all resolutions and check if there are resolutions that are not compatible across scopes
665
        for (const [symbol, resolutionDetails] of this.resolutionsMap.entries()) {
1,679✔
666
            if (resolutionDetails.size < 2) {
369✔
667
                // there is only one resolution... no worries
668
                continue;
176✔
669
            }
670
            const resolutionsList = [...resolutionDetails];
193✔
671
            const prime = resolutionsList[0];
193✔
672
            let incompatibleScopes = new Set<Scope>();
193✔
673
            let addedPrime = false;
193✔
674
            for (let i = 1; i < resolutionsList.length; i++) {
193✔
675
                let providedSymbolType = prime.providedSymbol.type;
256✔
676
                const symbolInThisScope = resolutionsList[i].providedSymbol;
256✔
677

678
                //get more general type
679
                if (providedSymbolType.isEqual(symbolInThisScope.type)) {
256✔
680
                    //type in this scope is the same as one we're already checking
681
                } else if (providedSymbolType.isTypeCompatible(symbolInThisScope.type)) {
13✔
682
                    //type in this scope is compatible with one we're storing. use most generic
683
                    providedSymbolType = symbolInThisScope.type;
1✔
684
                } else if (symbolInThisScope.type.isTypeCompatible(providedSymbolType)) {
12!
685
                    // type we're storing is more generic that the type in this scope
686
                } else {
687
                    // type in this scope is not compatible with other types for this symbol
688
                    if (!addedPrime) {
12✔
689
                        incompatibleScopes.add(prime.scope);
7✔
690
                        addedPrime = true;
7✔
691
                    }
692
                    incompatibleScopes.add(resolutionsList[i].scope);
12✔
693
                }
694
            }
695

696
            if (incompatibleScopes.size > 1) {
193✔
697
                incompatibleResolutions.push({
7✔
698
                    symbol: symbol,
699
                    incompatibleScopes: incompatibleScopes
700
                });
701
            }
702
        }
703
        return incompatibleResolutions;
1,679✔
704
    }
705

706
    private getCannotFindDiagnostic(scope: Scope, unresolvedSymbol: UnresolvedSymbol, typeChainResult: TypeChainProcessResult) {
707
        const parentDescriptor = this.getParentTypeDescriptor(this.getProvidedTree(scope)?.providedTree, typeChainResult);
235!
708
        const symbolType = typeChainResult.astNode?.getType({ flags: unresolvedSymbol.flags });
235!
709
        if (isReferenceType(symbolType)) {
235✔
710
            const circularReferenceInfo = symbolType.getCircularReferenceInfo();
229✔
711
            if (circularReferenceInfo.isCircularReference) {
229✔
712
                let diagnosticDetail = util.getCircularReferenceDiagnosticDetail(circularReferenceInfo, typeChainResult.fullNameOfItem);
7✔
713
                return DiagnosticMessages.circularReferenceDetected(diagnosticDetail);
7✔
714
            }
715
        }
716

717
        if (isCallExpression(typeChainResult.astNode?.parent) && typeChainResult.astNode?.parent.callee === typeChainResult.astNode) {
228!
718
            return DiagnosticMessages.cannotFindFunction(typeChainResult.itemName, typeChainResult.fullNameOfItem, typeChainResult.itemParentTypeName, parentDescriptor);
29✔
719
        }
720
        return DiagnosticMessages.cannotFindName(typeChainResult.itemName, typeChainResult.fullNameOfItem, typeChainResult.itemParentTypeName, parentDescriptor);
199✔
721
    }
722

723
    private getParentTypeDescriptor(provided: ProvidedNode, typeChainResult: TypeChainProcessResult) {
724
        if (typeChainResult.itemParentTypeKind === BscTypeKind.NamespaceType || provided?.getNamespace(typeChainResult.itemParentTypeName)) {
235!
725
            return 'namespace';
120✔
726
        }
727
        return 'type';
115✔
728
    }
729

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