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

rokucommunity / brighterscript / #13125

01 Oct 2024 02:12PM UTC coverage: 86.842% (-1.4%) from 88.193%
#13125

push

web-flow
Merge d4a9e5fff into 3a2dc7282

11554 of 14068 branches covered (82.13%)

Branch coverage included in aggregate %.

7000 of 7592 new or added lines in 100 files covered. (92.2%)

83 existing lines in 18 files now uncovered.

12701 of 13862 relevant lines covered (91.62%)

29529.09 hits per line

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

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

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

45

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

53
    getSymbol(symbolName: string): FileSymbolPair {
54
        if (!symbolName) {
1,891✔
55
            return;
746✔
56
        }
57
        const lowerSymbolName = symbolName.toLowerCase();
1,145✔
58
        if (this.componentsMap?.has(lowerSymbolName)) {
1,145!
NEW
59
            return this.componentsMap.get(lowerSymbolName);
×
60
        }
61
        let lowerSymbolNameParts = lowerSymbolName.split('.');
1,145✔
62
        return this.getSymbolByNameParts(lowerSymbolNameParts, this);
1,145✔
63
    }
64

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

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

80
            for (const namePart of rest) {
462✔
81
                if (isTypedFunctionType(currentType)) {
34✔
82
                    const returnType = currentType.returnType;
5✔
83
                    if (returnType.isResolvable()) {
5✔
84
                        currentType = returnType;
2✔
85
                    } else if (isReferenceType(returnType)) {
3!
86
                        const fullName = returnType.fullName;
3✔
87
                        if (fullName.includes('.')) {
3!
NEW
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];
34✔
96
                if (isEnumType(currentType)) {
34✔
97
                    typesToTry.push(currentType.defaultMemberType);
16✔
98
                }
99
                if (isInheritableType(currentType)) {
34✔
100
                    let inheritableType = currentType;
11✔
101
                    while (inheritableType?.parentType) {
11!
NEW
102
                        let parentType = inheritableType.parentType as BscType;
×
NEW
103
                        if (isReferenceType(inheritableType.parentType)) {
×
NEW
104
                            const fullName = inheritableType.parentType.fullName;
×
NEW
105
                            if (fullName.includes('.')) {
×
NEW
106
                                parentType = root.getSymbol(fullName)?.symbol?.type;
×
107
                            } else {
NEW
108
                                parentType = this.getSymbol(fullName)?.symbol?.type ??
×
109
                                    root.getSymbol(fullName)?.symbol?.type;
×
110
                            }
111
                        }
NEW
112
                        typesToTry.push(parentType);
×
NEW
113
                        inheritableType = parentType as InheritableType;
×
114
                    }
115

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

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

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

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

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

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

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

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

199

200
export class CrossScopeValidator {
1✔
201

202
    constructor(public program: Program) { }
1,741✔
203

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

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

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

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

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

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

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

260

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

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

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

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

285

286
        const addSymbolWithDuplicates = (symbolName: string, file: BscFile, symbolObj: ProvidedSymbol) => {
1,615✔
287
            // eslint-disable-next-line no-bitwise
288
            const globalSymbol = this.program.globalScope.symbolTable.getSymbol(symbolName, SymbolTypeFlag.typetime | SymbolTypeFlag.runtime);
3,906✔
289
            const symbolIsNamespace = providedTree.getNamespace(symbolName);
3,906✔
290
            const isDupe = providedTree.addSymbol(symbolName, { file: file, symbol: symbolObj.symbol });
3,906✔
291
            if (symbolIsNamespace || globalSymbol || isDupe || symbolObj.duplicates.length > 0) {
3,906✔
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,615✔
333
            for (const [_, nameMap] of file.providedSymbols.symbolMap.entries()) {
2,002✔
334

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

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

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

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

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

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

387
        const missingSymbols = new Set<UnresolvedSymbol>();
1,621✔
388

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

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

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

414
                if (foundNamespace) {
214✔
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 };
209✔
420
                    let namespaceNode = providedTree;
209✔
421
                    let currentKnownType;
422
                    for (const chainEntry of missing.typeChain) {
209✔
423
                        if (!chainEntry.isResolved) {
444✔
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;
224!
427
                            if (!currentKnownType) {
224!
428
                                namespaceNode = namespaceNode?.getNamespaceByNameParts([chainEntry.name]);
224!
429

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

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

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

463
    clearResolutionsForScopes(scopes: Scope[]) {
464
        for (const [symbol, resolutionInfos] of this.resolutionsMap.entries()) {
1,293✔
465
            for (const info of resolutionInfos.values()) {
138✔
466
                if (scopes.includes(info.scope)) {
144✔
467
                    resolutionInfos.delete(info);
133✔
468
                }
469
            }
470
            if (resolutionInfos.size === 0) {
138✔
471
                this.resolutionsMap.delete(symbol);
128✔
472
            }
473
        }
474
    }
475

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

480
        for (const scope of scopes) {
1,293✔
481
            scope.enumerateBrsFiles((file) => {
1,615✔
482
                if (filesThatNeedRevalidation.has(file) || filesThatDoNotNeedRevalidation.has(file)) {
2,002✔
483
                    return;
321✔
484
                }
485
                if (util.hasAnyRequiredSymbolChanged(file.requiredSymbols, changedSymbols)) {
1,681✔
486
                    filesThatNeedRevalidation.add(file);
178✔
487
                    return;
178✔
488
                }
489
                filesThatDoNotNeedRevalidation.add(file);
1,503✔
490
            });
491
        }
492
        return filesThatNeedRevalidation;
1,293✔
493
    }
494

495
    buildComponentsMap() {
496
        this.componentsMap.clear();
1,293✔
497
        // Add custom components
498
        for (let componentName of this.program.getSortedComponentNames()) {
1,293✔
499
            const typeName = 'rosgnode' + componentName;
443✔
500
            const component = this.program.getComponent(componentName);
443✔
501
            const componentSymbol = this.program.globalScope.symbolTable.getSymbol(typeName, SymbolTypeFlag.typetime)?.[0];
443✔
502
            if (componentSymbol && component) {
443✔
503
                this.componentsMap.set(typeName, { file: component.file, symbol: componentSymbol });
435✔
504
            }
505
        }
506
    }
507

508
    addDiagnosticsForScopes(scopes: Scope[]) { //, changedFiles: BrsFile[]) {
509
        const addDuplicateSymbolDiagnostics = true;
1,293✔
510
        const missingSymbolInScope = new Map<UnresolvedSymbol, Set<Scope>>();
1,293✔
511
        this.providedTreeMap.clear();
1,293✔
512
        this.clearResolutionsForScopes(scopes);
1,293✔
513

514
        // Check scope for duplicates and missing symbols
515
        for (const scope of scopes) {
1,293✔
516
            this.program.diagnostics.clearByFilter({
1,615✔
517
                scope: scope,
518
                tag: CrossScopeValidatorDiagnosticTag
519
            });
520

521
            const { missingSymbols, duplicatesMap } = this.getIssuesForScope(scope);
1,615✔
522
            if (addDuplicateSymbolDiagnostics) {
1,615!
523
                for (const [_flag, dupeSet] of duplicatesMap.entries()) {
1,615✔
524
                    if (dupeSet.size > 1) {
45!
525

526
                        const dupesArray = [...dupeSet.values()];
45✔
527

528
                        for (let i = 0; i < dupesArray.length; i++) {
45✔
529
                            const dupe = dupesArray[i];
136✔
530

531
                            const dupeNode = dupe?.symbol?.data?.definingNode;
136!
532
                            if (!dupeNode) {
136✔
533
                                continue;
15✔
534
                            }
535
                            let thisName = dupe.symbol?.name;
121!
536
                            const wrappingNameSpace = dupeNode?.findAncestor<NamespaceStatement>(isNamespaceStatement);
121!
537

538
                            if (wrappingNameSpace) {
121✔
539
                                thisName = `${wrappingNameSpace.getName(ParseMode.BrighterScript)}.` + thisName;
24✔
540
                            }
541

542
                            const thisNodeKindName = util.getAstNodeFriendlyName(dupeNode) ?? 'Item';
121!
543

544
                            for (let j = 0; j < dupesArray.length; j++) {
121✔
545
                                if (i === j) {
395✔
546
                                    continue;
121✔
547
                                }
548
                                const otherDupe = dupesArray[j];
274✔
549
                                if (!otherDupe || dupe.symbol === otherDupe.symbol) {
274✔
550
                                    continue;
98✔
551
                                }
552

553
                                const otherDupeNode = otherDupe.symbol.data?.definingNode;
176!
554
                                const otherIsGlobal = otherDupe.file.srcPath === 'global';
176✔
555

556
                                if (isFunctionStatement(dupeNode) && isFunctionStatement(otherDupeNode)) {
176✔
557
                                    // duplicate functions are handled in ScopeValidator
558
                                    continue;
26✔
559
                                }
560
                                if (otherIsGlobal &&
150✔
561
                                    (isInterfaceStatement(dupeNode) ||
562
                                        isEnumStatement(dupeNode) ||
563
                                        isConstStatement(dupeNode))) {
564
                                    // these are allowed to shadow global functions
565
                                    continue;
14✔
566
                                }
567
                                let thatName = otherDupe.symbol?.name;
136!
568

569
                                if (otherDupeNode) {
136✔
570
                                    const otherWrappingNameSpace = otherDupeNode?.findAncestor<NamespaceStatement>(isNamespaceStatement);
114!
571
                                    if (otherWrappingNameSpace) {
114✔
572
                                        thatName = `${otherWrappingNameSpace.getName(ParseMode.BrighterScript)}.` + thatName;
34✔
573
                                    }
574
                                }
575

576
                                type AstNodeWithName = VariableExpression | DottedGetExpression | EnumStatement | ClassStatement | ConstStatement | EnumMemberStatement | InterfaceStatement;
577

578
                                const thatNodeKindName = otherIsGlobal ? 'Global Function' : util.getAstNodeFriendlyName(otherDupeNode) ?? 'Item';
136!
579
                                let thisNameRange = (dupeNode as AstNodeWithName)?.tokens?.name?.location?.range ?? dupeNode.location?.range;
136!
580
                                let thatNameRange = (otherDupeNode as AstNodeWithName)?.tokens?.name?.location?.range ?? otherDupeNode?.location?.range;
136✔
581

582
                                const relatedInformation = thatNameRange ? [{
136✔
583
                                    message: `${thatNodeKindName} declared here`,
584
                                    location: util.createLocationFromFileRange(otherDupe.file, thatNameRange)
585
                                }] : undefined;
586
                                this.program.diagnostics.register({
136✔
587
                                    ...DiagnosticMessages.nameCollision(thisNodeKindName, thatNodeKindName, thatName),
588
                                    location: util.createLocationFromFileRange(dupe.file, thisNameRange),
589
                                    relatedInformation: relatedInformation
590
                                }, {
591
                                    scope: scope,
592
                                    tags: [CrossScopeValidatorDiagnosticTag]
593
                                });
594
                            }
595
                        }
596
                    }
597
                }
598
            }
599
            // build map of the symbols and scopes where the symbols are missing per file
600
            for (const missingSymbol of missingSymbols) {
1,615✔
601

602
                let scopesWithMissingSymbol = missingSymbolInScope.get(missingSymbol);
209✔
603
                if (!scopesWithMissingSymbol) {
209✔
604
                    scopesWithMissingSymbol = new Set<Scope>();
206✔
605
                    missingSymbolInScope.set(missingSymbol, scopesWithMissingSymbol);
206✔
606
                }
607
                scopesWithMissingSymbol.add(scope);
209✔
608
            }
609
        }
610

611
        // If symbols are missing in SOME scopes, add diagnostic
612
        for (const [symbol, scopeList] of missingSymbolInScope.entries()) {
1,293✔
613
            const typeChainResult = util.processTypeChain(symbol.typeChain);
206✔
614

615
            for (const scope of scopeList) {
206✔
616
                this.program.diagnostics.register({
209✔
617
                    ...this.getCannotFindDiagnostic(scope, typeChainResult),
618
                    location: typeChainResult.location
619
                }, {
620
                    scope: scope,
621
                    tags: [CrossScopeValidatorDiagnosticTag]
622
                });
623
            }
624
        }
625

626
        for (const resolution of this.getIncompatibleSymbolResolutions()) {
1,293✔
627
            const symbol = resolution.symbol;
5✔
628
            const incompatibleScopes = resolution.incompatibleScopes;
5✔
629
            if (incompatibleScopes.size > 1) {
5!
630
                const typeChainResult = util.processTypeChain(symbol.typeChain);
5✔
631
                const scopeListName = [...incompatibleScopes.values()].map(s => s.name).join(', ');
10✔
632
                this.program.diagnostics.register({
5✔
633
                    ...DiagnosticMessages.incompatibleSymbolDefinition(typeChainResult.fullChainName, scopeListName),
634
                    location: typeChainResult.location
635
                }, {
636
                    tags: [CrossScopeValidatorDiagnosticTag]
637
                });
638
            }
639
        }
640
    }
641

642
    getIncompatibleSymbolResolutions() {
643
        const incompatibleResolutions = new Array<{ symbol: UnresolvedSymbol; incompatibleScopes: Set<Scope> }>();
1,294✔
644
        // check all resolutions and check if there are resolutions that are not compatible across scopes
645
        for (const [symbol, resolutionDetails] of this.resolutionsMap.entries()) {
1,294✔
646
            if (resolutionDetails.size < 2) {
335✔
647
                // there is only one resolution... no worries
648
                continue;
306✔
649
            }
650
            const resolutionsList = [...resolutionDetails];
29✔
651
            const prime = resolutionsList[0];
29✔
652
            let incompatibleScopes = new Set<Scope>();
29✔
653
            let addedPrime = false;
29✔
654
            for (let i = 1; i < resolutionsList.length; i++) {
29✔
655
                let providedSymbolType = prime.providedSymbol.type;
29✔
656
                const symbolInThisScope = resolutionsList[i].providedSymbol;
29✔
657

658
                //get more general type
659
                if (providedSymbolType.isEqual(symbolInThisScope.type)) {
29✔
660
                    //type in this scope is the same as one we're already checking
661
                } else if (providedSymbolType.isTypeCompatible(symbolInThisScope.type)) {
6!
662
                    //type in this scope is compatible with one we're storing. use most generic
NEW
663
                    providedSymbolType = symbolInThisScope.type;
×
664
                } else if (symbolInThisScope.type.isTypeCompatible(providedSymbolType)) {
6!
665
                    // type we're storing is more generic that the type in this scope
666
                } else {
667
                    // type in this scope is not compatible with other types for this symbol
668
                    if (!addedPrime) {
6!
669
                        incompatibleScopes.add(prime.scope);
6✔
670
                        addedPrime = true;
6✔
671
                    }
672
                    incompatibleScopes.add(resolutionsList[i].scope);
6✔
673
                }
674
            }
675

676
            if (incompatibleScopes.size > 1) {
29✔
677
                incompatibleResolutions.push({
6✔
678
                    symbol: symbol,
679
                    incompatibleScopes: incompatibleScopes
680
                });
681
            }
682
        }
683
        return incompatibleResolutions;
1,294✔
684
    }
685

686
    private getCannotFindDiagnostic(scope: Scope, typeChainResult: TypeChainProcessResult) {
687
        const parentDescriptor = this.getParentTypeDescriptor(this.getProvidedTree(scope)?.providedTree, typeChainResult);
209!
688
        if (isCallExpression(typeChainResult.astNode?.parent) && typeChainResult.astNode?.parent.callee === typeChainResult.astNode) {
209!
689
            return DiagnosticMessages.cannotFindFunction(typeChainResult.itemName, typeChainResult.fullNameOfItem, typeChainResult.itemParentTypeName, parentDescriptor);
29✔
690
        }
691
        return DiagnosticMessages.cannotFindName(typeChainResult.itemName, typeChainResult.fullNameOfItem, typeChainResult.itemParentTypeName, parentDescriptor);
180✔
692
    }
693

694
    private getParentTypeDescriptor(provided: ProvidedNode, typeChainResult: TypeChainProcessResult) {
695
        if (typeChainResult.itemParentTypeKind === BscTypeKind.NamespaceType || provided?.getNamespace(typeChainResult.itemParentTypeName)) {
209!
696
            return 'namespace';
118✔
697
        }
698
        return 'type';
91✔
699
    }
700

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