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

rokucommunity / brighterscript / #14412

14 May 2025 06:21PM UTC coverage: 87.001%. Remained the same
#14412

push

web-flow
Merge dcac82f0a into a194c3925

13885 of 16867 branches covered (82.32%)

Branch coverage included in aggregate %.

170 of 187 new or added lines in 11 files covered. (90.91%)

136 existing lines in 10 files now uncovered.

14746 of 16042 relevant lines covered (91.92%)

21905.94 hits per line

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

73.02
/src/Scope.ts
1
/* eslint-disable @typescript-eslint/dot-notation */
2
import * as path from 'path';
1✔
3
import chalk from 'chalk';
1✔
4
import type { CallableContainer, FileReference, FileLink, Callable, NamespaceContainer, ScopeValidationOptions, ScopeNamespaceContainer } from './interfaces';
5
import type { Program } from './Program';
6
import { type NamespaceStatement, type ClassStatement, type EnumStatement, type InterfaceStatement, type EnumMemberStatement, type ConstStatement } from './parser/Statement';
7
import { ParseMode } from './parser/Parser';
1✔
8
import { util } from './util';
1✔
9
import { Cache } from './Cache';
1✔
10
import type { BrsFile } from './files/BrsFile';
11
import type { DependencyGraph, DependencyChangedEvent } from './DependencyGraph';
12
import { isBrsFile, isXmlFile, isEnumMemberStatement, isXmlScope } from './astUtils/reflection';
1✔
13
import { SymbolTable } from './SymbolTable';
1✔
14
import { SymbolTypeFlag } from './SymbolTypeFlag';
1✔
15
import type { BscFile } from './files/BscFile';
16
import { referenceTypeFactory } from './types/ReferenceType';
1✔
17
import { unionTypeFactory } from './types/UnionType';
1✔
18
import { AssociativeArrayType } from './types/AssociativeArrayType';
1✔
19
import type { Statement } from './parser/AstNode';
20
import { performance } from 'perf_hooks';
1✔
21
import { LogLevel } from './logging';
1✔
22
import { uninitializedTypeFactory } from './types/UninitializedType';
1✔
23

24
/**
25
 * Assign some few factories to the SymbolTable to prevent cyclical imports. This file seems like the most intuitive place to do the linking
26
 * since Scope will be used by pretty much everything
27
 */
28
SymbolTable.referenceTypeFactory = referenceTypeFactory;
1✔
29
SymbolTable.unionTypeFactory = unionTypeFactory;
1✔
30
SymbolTable.uninitializedTypeFactory = uninitializedTypeFactory;
1✔
31
/**
32
 * A class to keep track of all declarations within a given scope (like source scope, component scope)
33
 */
34
export class Scope {
1✔
35
    constructor(
36
        public name: string,
4,591✔
37
        public program: Program,
4,591✔
38
        private _dependencyGraphKey?: string
4,591✔
39
    ) {
40
        this.isValidated = false;
4,591✔
41
        //used for improved logging performance
42
        this._debugLogComponentName = `Scope '${chalk.redBright(this.name)}'`;
4,591✔
43
    }
44

45
    /**
46
     * Indicates whether this scope needs to be validated.
47
     * Will be true when first constructed, or anytime one of its dependencies changes
48
     */
49
    public readonly isValidated: boolean;
50

51
    protected cache = new Cache();
4,591✔
52

53
    public get dependencyGraphKey() {
54
        return this._dependencyGraphKey;
8,734✔
55
    }
56

57
    /**
58
     * A dictionary of namespaces, indexed by the lower case full name of each namespace.
59
     * If a namespace is declared as "NameA.NameB.NameC", there will be 3 entries in this dictionary,
60
     * "namea", "namea.nameb", "namea.nameb.namec"
61
     */
62
    public get namespaceLookup() {
63
        let allFilesValidated = true;
6,684✔
64
        for (const file of this.getAllFiles()) {
6,684✔
65
            if (isBrsFile(file) && !file.hasTypedef) {
8,643✔
66
                allFilesValidated = allFilesValidated && file.isValidated;
7,428✔
67
                if (!allFilesValidated) {
7,428✔
68
                    break;
876✔
69
                }
70
            }
71
        }
72
        if (!allFilesValidated) {
6,684✔
73
            // This is not fit to cache
74
            // Since the files have not been validated, all namespace info might not have been available
75
            return this.buildNamespaceLookup();
876✔
76
        }
77
        return this.cache.getOrAdd('namespaceLookup', () => this.buildNamespaceLookup());
5,808✔
78
    }
79

80
    /**
81
     * A dictionary of namespaces, indexed by the lower case full name of each namespace.
82
     * If a namespace is declared as "NameA.NameB.NameC", there will be 3 entries in this dictionary,
83
     * "namea", "namea.nameb", "namea.nameb.namec"
84
     */
85
    public get namespaceNameSet() {
86
        return this.cache.getOrAdd('namespaceNameSet', () => {
1✔
87
            const lowerNamespaceNames = new Set<string>();
1✔
88

89
            this.enumerateBrsFiles((file) => {
1✔
90
                const fileNamespaceLookup = file.getNamespaceLookupObject();
3✔
91
                for (const [lowerNamespaceName, _] of fileNamespaceLookup) {
3✔
92
                    lowerNamespaceNames.add(lowerNamespaceName);
2✔
93
                }
94
            });
95
            return lowerNamespaceNames;
1✔
96
        });
97
    }
98

99

100
    /**
101
     * Get a NamespaceContainer by its name, looking for a fully qualified version first, then global version next if not found
102
     */
103
    public getNamespace(name: string, containingNamespace?: string) {
104
        const nameLower = name?.toLowerCase();
50!
105
        const lookup = this.namespaceLookup;
50✔
106

107
        let ns: NamespaceContainer;
108
        if (containingNamespace) {
50!
UNCOV
109
            ns = lookup.get(`${containingNamespace?.toLowerCase()}.${nameLower}`)?.firstInstance;
×
110
        }
111
        //if we couldn't find the namespace by its full namespaced name, look for a global version
112
        if (!ns) {
50!
113
            ns = lookup.get(nameLower)?.firstInstance;
50✔
114
        }
115
        return ns;
50✔
116
    }
117

118
    /**
119
     * Get a NamespaceContainer by its name, looking for a fully qualified version first, then global version next if not found
120
     */
121
    public getNamespacesWithRoot(rootName: string, containingNamespace?: string) {
UNCOV
122
        const nameLower = rootName?.toLowerCase();
×
123
        const lookup = this.namespaceLookup;
×
124
        const lookupKeys = [...lookup.keys()];
×
125
        let lookupName = nameLower;
×
126
        if (containingNamespace) {
×
127
            lookupName = `${containingNamespace?.toLowerCase()}.${nameLower}`;
×
128
        }
UNCOV
129
        const nsList = lookupKeys.filter(key => key === lookupName).map(key => lookup.get(key));
×
130
        return nsList;
×
131
    }
132

133

134
    /**
135
     * Get a NamespaceContainer by its name, looking for a fully qualified version first, then global version next if not found
136
     */
137
    public getFirstNamespaceWithRoot(rootName: string, containingNamespace?: string) {
UNCOV
138
        const nameLower = rootName?.toLowerCase();
×
139

UNCOV
140
        let lookupName = nameLower;
×
141
        if (containingNamespace) {
×
142
            lookupName = `${containingNamespace?.toLowerCase()}.${nameLower}`;
×
143
        }
UNCOV
144
        return this.namespaceLookup.get(lookupName)?.firstInstance;
×
145
    }
146

147
    /**
148
     * Get the class with the specified name.
149
     * @param className - The class name, including the namespace of the class if possible
150
     * @param containingNamespace - The namespace used to resolve relative class names. (i.e. the namespace around the current statement trying to find a class)
151
     */
152
    public getClass(className: string, containingNamespace?: string): ClassStatement {
153
        return this.getClassFileLink(className, containingNamespace)?.item;
8!
154
    }
155

156
    /**
157
     * Get the interface with the specified name.
158
     * @param ifaceName - The interface name, including the namespace of the interface if possible
159
     * @param containingNamespace - The namespace used to resolve relative interface names. (i.e. the namespace around the current statement trying to find a interface)
160
     */
161
    public getInterface(ifaceName: string, containingNamespace?: string): InterfaceStatement {
UNCOV
162
        return this.getInterfaceFileLink(ifaceName, containingNamespace)?.item;
×
163
    }
164

165
    /**
166
     * Get the enum with the specified name.
167
     * @param enumName - The enum name, including the namespace if possible
168
     * @param containingNamespace - The namespace used to resolve relative enum names. (i.e. the namespace around the current statement trying to find an enum)
169
     */
170
    public getEnum(enumName: string, containingNamespace?: string): EnumStatement {
171
        return this.getEnumFileLink(enumName, containingNamespace)?.item;
27✔
172
    }
173

174
    private useFileCachesForFileLinkLookups = false;
4,591✔
175

176
    private getFileLinkFromFileMap<T>(cachedMapName: string, itemName: string, containingNamespace?: string): FileLink<T> {
177
        let result: FileLink<T>;
178
        const fullNameLower = util.getFullyQualifiedClassName(itemName, containingNamespace)?.toLowerCase();
283!
179
        const itemNameLower = itemName?.toLowerCase();
283!
180
        if (fullNameLower) {
283✔
181
            this.enumerateBrsFilesWithBreak((file) => {
258✔
182
                let stmt = file['_cachedLookups'][cachedMapName].get(fullNameLower);
381✔
183
                if (stmt) {
381✔
184
                    result = { item: stmt, file: file };
3✔
185
                }
186
                return !!stmt;
381✔
187
            });
188
        }
189
        if (!result && itemNameLower && fullNameLower !== itemNameLower) {
283!
UNCOV
190
            this.enumerateBrsFilesWithBreak((file) => {
×
191
                let stmt = file['_cachedLookups'][cachedMapName].get(itemNameLower);
×
192
                if (stmt) {
×
193
                    result = { item: stmt, file: file };
×
194
                }
UNCOV
195
                return !!stmt;
×
196
            });
197
        }
198
        return result;
283✔
199
    }
200

201
    /**
202
     * Get a class and its containing file by the class name
203
     * @param className - The class name, including the namespace of the class if possible
204
     * @param containingNamespace - The namespace used to resolve relative class names. (i.e. the namespace around the current statement trying to find a class)
205
     */
206
    public getClassFileLink(className: string, containingNamespace?: string): FileLink<ClassStatement> {
207
        if (this.useFileCachesForFileLinkLookups) {
2,208✔
208
            return this.getFileLinkFromFileMap('classStatementMap', className, containingNamespace);
184✔
209
        }
210
        const lowerName = className?.toLowerCase();
2,024✔
211
        const fullNameLower = util.getFullyQualifiedClassName(lowerName, containingNamespace)?.toLowerCase();
2,024✔
212
        const classMap = this.getClassMap();
2,024✔
213

214
        let cls = classMap.get(fullNameLower);
2,024✔
215
        //if we couldn't find the class by its full namespaced name, look for a global class with that name
216
        if (!cls && lowerName && lowerName !== fullNameLower) {
2,024✔
217
            cls = classMap.get(lowerName);
6✔
218
        }
219
        return cls;
2,024✔
220
    }
221

222
    /**
223
     * Get an interface and its containing file by the interface name
224
     * @param ifaceName - The interface name, including the namespace of the interface if possible
225
     * @param containingNamespace - The namespace used to resolve relative interface names. (i.e. the namespace around the current statement trying to find a interface)
226
     */
227
    public getInterfaceFileLink(ifaceName: string, containingNamespace?: string): FileLink<InterfaceStatement> {
228
        if (this.useFileCachesForFileLinkLookups) {
8!
UNCOV
229
            return this.getFileLinkFromFileMap('interfaceStatementMap', ifaceName, containingNamespace);
×
230
        }
231
        const lowerName = ifaceName?.toLowerCase();
8✔
232
        const fullNameLower = util.getFullyQualifiedClassName(lowerName, containingNamespace)?.toLowerCase();
8✔
233
        const ifaceMap = this.getInterfaceMap();
8✔
234

235
        let iface = ifaceMap.get(fullNameLower);
8✔
236
        //if we couldn't find the iface by its full namespaced name, look for a global class with that name
237
        if (!iface && lowerName && lowerName !== fullNameLower) {
8!
UNCOV
238
            iface = ifaceMap.get(lowerName);
×
239
        }
240
        return iface;
8✔
241
    }
242

243
    /**
244
     * Get an Enum and its containing file by the Enum name
245
     * @param enumName - The Enum name, including the namespace of the enum if possible
246
     * @param containingNamespace - The namespace used to resolve relative enum names. (i.e. the namespace around the current statement trying to find a enum)
247
     */
248
    public getEnumFileLink(enumName: string, containingNamespace?: string): FileLink<EnumStatement> {
249
        if (this.useFileCachesForFileLinkLookups) {
1,326✔
250
            return this.getFileLinkFromFileMap('enumStatementMap', enumName, containingNamespace);
67✔
251
        }
252
        const lowerName = enumName?.toLowerCase();
1,259✔
253
        const fullNameLower = util.getFullyQualifiedClassName(lowerName, containingNamespace)?.toLowerCase();
1,259✔
254
        const enumMap = this.getEnumMap();
1,259✔
255

256
        let enumeration = enumMap.get(fullNameLower);
1,259✔
257
        //if we couldn't find the enum by its full namespaced name, look for a global enum with that name
258
        if (!enumeration && lowerName && lowerName !== fullNameLower) {
1,259✔
259
            enumeration = enumMap.get(lowerName);
67✔
260
        }
261
        return enumeration;
1,259✔
262
    }
263

264
    /**
265
     * Get an Enum and its containing file by the Enum name
266
     * @param enumMemberName - The Enum name, including the namespace of the enum if possible
267
     * @param containingNamespace - The namespace used to resolve relative enum names. (i.e. the namespace around the current statement trying to find a enum)
268
     */
269
    public getEnumMemberFileLink(enumMemberName: string, containingNamespace?: string): FileLink<EnumMemberStatement> {
270
        let lowerNameParts = enumMemberName?.toLowerCase()?.split('.');
10✔
271
        let memberName = lowerNameParts?.splice(lowerNameParts.length - 1, 1)?.[0];
10✔
272
        let lowerName = lowerNameParts?.join('.').toLowerCase();
10✔
273
        const enumMap = this.getEnumMap();
10✔
274

275
        let enumeration = enumMap.get(
10✔
276
            util.getFullyQualifiedClassName(lowerName, containingNamespace?.toLowerCase())
30!
277
        );
278
        //if we couldn't find the enum by its full namespaced name, look for a global enum with that name
279
        if (!enumeration) {
10✔
280
            enumeration = enumMap.get(lowerName);
9✔
281
        }
282
        if (enumeration) {
10✔
283
            let member = enumeration.item.findChild<EnumMemberStatement>((child) => isEnumMemberStatement(child) && child.name?.toLowerCase() === memberName);
1!
284
            return member ? { item: member, file: enumeration.file } : undefined;
1!
285
        }
286
    }
287

288
    /**
289
     * Get a constant and its containing file by the constant name
290
     * @param constName - The constant name, including the namespace of the constant if possible
291
     * @param containingNamespace - The namespace used to resolve relative constant names. (i.e. the namespace around the current statement trying to find a constant)
292
     */
293
    public getConstFileLink(constName: string, containingNamespace?: string): FileLink<ConstStatement> {
294
        if (this.useFileCachesForFileLinkLookups) {
761✔
295
            return this.getFileLinkFromFileMap('constStatementMap', constName, containingNamespace);
32✔
296
        }
297
        const lowerName = constName?.toLowerCase();
729✔
298
        const fullNameLower = util.getFullyQualifiedClassName(lowerName, containingNamespace)?.toLowerCase();
729✔
299

300
        const constMap = this.getConstMap();
729✔
301

302
        let result = constMap.get(fullNameLower);
729✔
303
        //if we couldn't find the constant by its full namespaced name, look for a global constant with that name
304
        if (!result && lowerName !== fullNameLower) {
729✔
305
            result = constMap.get(lowerName);
61✔
306
        }
307
        return result;
729✔
308
    }
309

310
    public getAllFileLinks(name: string, containingNamespace?: string, includeNamespaces = false, includeNameShadowsOutsideNamespace = false): FileLink<Statement>[] {
×
UNCOV
311
        let links: FileLink<Statement>[] = [];
×
312

UNCOV
313
        links.push(this.getClassFileLink(name, containingNamespace),
×
314
            this.getInterfaceFileLink(name, containingNamespace),
315
            this.getConstFileLink(name, containingNamespace),
316
            this.getEnumFileLink(name, containingNamespace));
UNCOV
317
        if (includeNameShadowsOutsideNamespace && containingNamespace) {
×
318
            links.push(this.getClassFileLink(name),
×
319
                this.getInterfaceFileLink(name),
320
                this.getConstFileLink(name),
321
                this.getEnumFileLink(name));
322
        }
UNCOV
323
        if (includeNamespaces) {
×
324
            const nameSpaceContainer = this.getFirstNamespaceWithRoot(name, containingNamespace);
×
325
            if (nameSpaceContainer) {
×
326
                links.push({ item: nameSpaceContainer.namespaceStatements?.[0], file: nameSpaceContainer?.file as BrsFile });
×
327
            }
328
        }
UNCOV
329
        const fullNameLower = (containingNamespace ? `${containingNamespace}.${name}` : name).toLowerCase();
×
330
        const callable = this.getCallableByName(name);
×
331
        if (callable) {
×
332
            if ((!callable.hasNamespace && includeNameShadowsOutsideNamespace) || callable.getName(ParseMode.BrighterScript).toLowerCase() === fullNameLower) {
×
333
                // this callable has no namespace, or has same namespace
UNCOV
334
                links.push({ item: callable.functionStatement, file: callable.file as BrsFile });
×
335
            }
336
        }
337
        // remove empty links
UNCOV
338
        return links.filter(link => link);
×
339
    }
340

341
    /**
342
     * Get a map of all enums by their member name.
343
     * The keys are lower-case fully-qualified paths to the enum and its member. For example:
344
     * namespace.enum.value
345
     */
346
    public getEnumMemberMap() {
UNCOV
347
        return this.cache.getOrAdd('enumMemberMap', () => {
×
348
            const result = new Map<string, EnumMemberStatement>();
×
349
            for (const [key, eenum] of this.getEnumMap()) {
×
350
                for (const member of eenum.item.getMembers()) {
×
351
                    result.set(`${key}.${member.name.toLowerCase()}`, member);
×
352
                }
353
            }
UNCOV
354
            return result;
×
355
        });
356
    }
357

358
    /**
359
     * Tests if a class exists with the specified name
360
     * @param className - the all-lower-case namespace-included class name
361
     * @param namespaceName - The namespace used to resolve relative class names. (i.e. the namespace around the current statement trying to find a class)
362
     */
363
    public hasClass(className: string, namespaceName?: string): boolean {
UNCOV
364
        return !!this.getClass(className, namespaceName);
×
365
    }
366

367
    /**
368
     * Tests if an interface exists with the specified name
369
     * @param ifaceName - the all-lower-case namespace-included interface name
370
     * @param namespaceName - the current namespace name
371
     */
372
    public hasInterface(ifaceName: string, namespaceName?: string): boolean {
UNCOV
373
        return !!this.getInterface(ifaceName, namespaceName);
×
374
    }
375

376
    /**
377
     * Tests if an enum exists with the specified name
378
     * @param enumName - the all-lower-case namespace-included enum name
379
     * @param namespaceName - the current namespace name
380
     */
381
    public hasEnum(enumName: string, namespaceName?: string): boolean {
UNCOV
382
        return !!this.getEnum(enumName, namespaceName);
×
383
    }
384

385
    /**
386
     * A dictionary of all classes in this scope. This includes namespaced classes always with their full name.
387
     * The key is stored in lower case
388
     */
389
    public getClassMap(): Map<string, FileLink<ClassStatement>> {
390
        return this.cache.getOrAdd('classMap', () => {
2,024✔
391
            const map = new Map<string, FileLink<ClassStatement>>();
879✔
392
            this.enumerateBrsFiles((file) => {
879✔
393
                if (isBrsFile(file)) {
1,069!
394
                    for (let cls of file['_cachedLookups'].classStatements) {
1,069✔
395
                        const className = cls.getName(ParseMode.BrighterScript);
239✔
396
                        //only track classes with a defined name (i.e. exclude nameless malformed classes)
397
                        if (className) {
239!
398
                            map.set(className.toLowerCase(), { item: cls, file: file });
239✔
399
                        }
400
                    }
401
                }
402
            });
403
            return map;
879✔
404
        });
405
    }
406

407
    /**
408
     * A dictionary of all Interfaces in this scope. This includes namespaced Interfaces always with their full name.
409
     * The key is stored in lower case
410
     */
411
    public getInterfaceMap(): Map<string, FileLink<InterfaceStatement>> {
412
        return this.cache.getOrAdd('interfaceMap', () => {
8✔
413
            const map = new Map<string, FileLink<InterfaceStatement>>();
8✔
414
            this.enumerateBrsFiles((file) => {
8✔
415
                if (isBrsFile(file)) {
11!
416
                    for (let iface of file['_cachedLookups'].interfaceStatements) {
11✔
417
                        const ifaceName = iface.getName(ParseMode.BrighterScript);
2✔
418
                        //only track classes with a defined name (i.e. exclude nameless malformed classes)
419
                        if (ifaceName) {
2!
420
                            map.set(ifaceName.toLowerCase(), { item: iface, file: file });
2✔
421
                        }
422
                    }
423
                }
424
            });
425
            return map;
8✔
426
        });
427
    }
428

429
    /**
430
     * A dictionary of all enums in this scope. This includes namespaced enums always with their full name.
431
     * The key is stored in lower case
432
     */
433
    public getEnumMap(): Map<string, FileLink<EnumStatement>> {
434
        return this.cache.getOrAdd('enumMap', () => {
1,323✔
435
            const map = new Map<string, FileLink<EnumStatement>>();
194✔
436
            this.enumerateBrsFiles((file) => {
194✔
437
                for (let enumStmt of file['_cachedLookups'].enumStatements) {
225✔
438
                    //only track enums with a defined name (i.e. exclude nameless malformed enums)
439
                    if (enumStmt.fullName) {
62!
440
                        map.set(enumStmt.fullName.toLowerCase(), { item: enumStmt, file: file });
62✔
441
                    }
442
                }
443
            });
444
            return map;
194✔
445
        });
446
    }
447

448
    /**
449
     * A dictionary of all constants in this scope. This includes namespaced constants always with their full name.
450
     * The key is stored in lower case
451
     */
452
    public getConstMap(): Map<string, FileLink<ConstStatement>> {
453
        return this.cache.getOrAdd('constMap', () => {
779✔
454
            const map = new Map<string, FileLink<ConstStatement>>();
215✔
455
            this.enumerateBrsFiles((file) => {
215✔
456
                for (let stmt of file['_cachedLookups'].constStatements) {
244✔
457
                    //only track enums with a defined name (i.e. exclude nameless malformed enums)
458
                    if (stmt.fullName) {
29!
459
                        map.set(stmt.fullName.toLowerCase(), { item: stmt, file: file });
29✔
460
                    }
461
                }
462
            });
463
            return map;
215✔
464
        });
465
    }
466

467
    protected onDependenciesChanged(event: DependencyChangedEvent) {
468
        this.logDebug('invalidated because dependency graph said [', event.sourceKey, '] changed');
2,568✔
469
        this.invalidate();
2,568✔
470
    }
471

472
    /**
473
     * Clean up all event handles
474
     */
475
    public dispose() {
476
        this.unsubscribeFromDependencyGraph?.();
5,840!
477
    }
478

479
    /**
480
     * Does this scope know about the given namespace name?
481
     * @param namespaceName - the name of the namespace (i.e. "NameA", or "NameA.NameB", etc...)
482
     */
483
    public isKnownNamespace(namespaceName: string) {
UNCOV
484
        let namespaceNameLower = namespaceName.toLowerCase();
×
485
        this.enumerateBrsFiles((file) => {
×
486
            for (let namespace of file['_cachedLookups'].namespaceStatements) {
×
487
                let loopNamespaceNameLower = namespace.name.toLowerCase();
×
488
                if (loopNamespaceNameLower === namespaceNameLower || loopNamespaceNameLower.startsWith(namespaceNameLower + '.')) {
×
489
                    return true;
×
490
                }
491
            }
492
        });
UNCOV
493
        return false;
×
494
    }
495

496
    /**
497
     * Get the parent scope for this scope (for source scope this will always be the globalScope).
498
     * XmlScope overrides this to return the parent xml scope if available.
499
     * For globalScope this will return null.
500
     */
501
    public getParentScope(): Scope | null {
502
        let scope: Scope | undefined;
503
        //use the global scope if we didn't find a sope and this is not the global scope
504
        if (this.program.globalScope !== this) {
349,216✔
505
            scope = this.program.globalScope;
8,992✔
506
        }
507
        if (scope) {
349,216✔
508
            return scope;
8,992✔
509
        } else {
510
            //passing null to the cache allows it to skip the factory function in the future
511
            return null;
340,224✔
512
        }
513
    }
514

515
    private dependencyGraph: DependencyGraph;
516
    /**
517
     * An unsubscribe function for the dependencyGraph subscription
518
     */
519
    private unsubscribeFromDependencyGraph: () => void;
520

521
    public attachDependencyGraph(dependencyGraph: DependencyGraph) {
522
        this.dependencyGraph = dependencyGraph;
4,205✔
523
        if (this.unsubscribeFromDependencyGraph) {
4,205✔
524
            this.unsubscribeFromDependencyGraph();
2✔
525
        }
526

527
        //anytime a dependency for this scope changes, we need to be revalidated
528
        this.unsubscribeFromDependencyGraph = this.dependencyGraph.onchange(this.dependencyGraphKey, this.onDependenciesChanged.bind(this));
4,205✔
529

530
        //invalidate immediately since this is a new scope
531
        this.invalidate();
4,205✔
532
    }
533

534
    /**
535
     * Get the file from this scope with the given path.
536
     * @param filePath can be a srcPath or destPath
537
     * @param normalizePath should this function repair and standardize the path? Passing false should have a performance boost if you can guarantee your path is already sanitized
538
     */
539
    public getFile<TFile extends BscFile>(filePath: string, normalizePath = true) {
11✔
540
        if (typeof filePath !== 'string') {
11!
UNCOV
541
            return undefined;
×
542
        }
543

544
        const key: keyof Pick<BscFile, 'srcPath' | 'destPath'> = path.isAbsolute(filePath) ? 'srcPath' : 'destPath';
11✔
545
        let map = this.cache.getOrAdd('fileMaps-srcPath', () => {
11✔
546
            const result = new Map<string, BscFile>();
7✔
547
            for (const file of this.getAllFiles()) {
7✔
548
                result.set(file[key].toLowerCase(), file);
11✔
549
            }
550
            return result;
7✔
551
        });
552
        return map.get(
11✔
553
            (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase()
11!
554
        ) as TFile;
555
    }
556

557
    /**
558
     * Get the list of files referenced by this scope that are actually loaded in the program.
559
     * Excludes files from ancestor scopes
560
     */
561
    public getOwnFiles() {
562
        //source scope only inherits files from global, so just return all files. This function mostly exists to assist XmlScope
563
        return this.getAllFiles();
26,799✔
564
    }
565

566
    /**
567
     * Get the list of files referenced by this scope that are actually loaded in the program.
568
     * Includes files from this scope and all ancestor scopes
569
     */
570
    public getAllFiles(): BscFile[] {
571
        return this.cache.getOrAdd('getAllFiles', () => {
37,254✔
572
            let result = [] as BscFile[];
5,981✔
573
            let dependencies = this.dependencyGraph.getAllDependencies(this.dependencyGraphKey);
5,981✔
574
            for (let dependency of dependencies) {
5,981✔
575
                //load components by their name
576
                if (dependency.startsWith('component:')) {
8,005✔
577
                    let comp = this.program.getComponent(dependency.replace(/^component:/, ''));
1,018✔
578
                    if (comp) {
1,018✔
579
                        result.push(comp.file);
65✔
580
                    }
581
                } else {
582
                    let file = this.program.getFile(dependency);
6,987✔
583
                    if (file) {
6,987✔
584
                        result.push(file);
5,092✔
585
                    }
586
                }
587
            }
588
            return result;
5,981✔
589
        });
590
    }
591

592
    /**
593
     * Gets a list of all files in this scope, but not imported files, and not from ancestor scopes
594
     */
595
    public getImmediateFiles(): BscFile[] {
UNCOV
596
        return this.cache.getOrAdd('getImmediateFiles', () => {
×
597
            let result = [] as BscFile[];
×
598
            if (isXmlScope(this)) {
×
599
                result.push(this.xmlFile);
×
600
            }
UNCOV
601
            let dependencies = this.dependencyGraph.getImmediateDependencies(this.dependencyGraphKey);
×
602
            for (let dependency of dependencies) {
×
603
                //load components by their name
UNCOV
604
                if (dependency.startsWith('component:')) {
×
605
                    let comp = this.program.getComponent(dependency.replace(/^component:/, ''));
×
606
                    if (comp) {
×
607
                        result.push(...comp.scope.getImmediateFiles());
×
608
                        result.push(comp.file);
×
609
                    }
610
                } else {
UNCOV
611
                    let file = this.program.getFile(dependency);
×
612
                    if (file) {
×
613
                        result.push(file);
×
614
                    }
615
                }
616
            }
UNCOV
617
            this.logDebug('getImmediateFiles', () => result.map(x => x.destPath));
×
618
            return result;
×
619
        });
620
    }
621

622
    /**
623
     * Get the list of callables available in this scope (either declared in this scope or in a parent scope)
624
     */
625
    public getAllCallables(): CallableContainer[] {
626
        //get callables from parent scopes
627
        let parentScope = this.getParentScope();
3,926✔
628
        if (parentScope) {
3,926✔
629
            return [...this.getOwnCallables(), ...parentScope.getAllCallables()];
1,980✔
630
        } else {
631
            return [...this.getOwnCallables()];
1,946✔
632
        }
633
    }
634

635
    /**
636
     * Get the callable with the specified name.
637
     * If there are overridden callables with the same name, the closest callable to this scope is returned
638
     */
639
    public getCallableByName(name: string) {
UNCOV
640
        return this.getCallableMap().get(
×
641
            name.toLowerCase()
642
        );
643
    }
644

645
    public getCallableMap() {
UNCOV
646
        return this.cache.getOrAdd('callableMap', () => {
×
647
            const result = new Map<string, Callable>();
×
648
            for (let callable of this.getAllCallables()) {
×
649
                const callableName = callable.callable.getName(ParseMode.BrighterScript)?.toLowerCase();
×
650
                result.set(callableName, callable.callable);
×
651
                result.set(
×
652
                    // Split by `.` and check the last term to consider namespaces.
653
                    callableName.split('.').pop()?.toLowerCase(),
×
654
                    callable.callable
655
                );
656
            }
UNCOV
657
            return result;
×
658
        });
659
    }
660

661
    public getCallableContainerMap() {
662
        return this.cache.getOrAdd('callableContainerMap', () => {
3,954✔
663
            let callables = this.getAllCallables();
1,929✔
664

665
            //get a list of all callables, indexed by their lower case names
666
            return util.getCallableContainersByLowerName(callables);
1,929✔
667
        });
668
    }
669

670
    /**
671
     * Iterate over Brs files not shadowed by typedefs
672
     */
673
    public enumerateBrsFiles(callback: (file: BrsFile) => void) {
674
        const files = this.getAllFiles();
23,936✔
675
        for (const file of files) {
23,936✔
676
            //only brs files without a typedef
677
            if (isBrsFile(file) && !file.hasTypedef) {
33,386✔
678
                callback(file);
27,751✔
679
            }
680
        }
681
    }
682

683
    /**
684
     * Iterate over Brs files not shadowed by typedefs
685
     */
686
    public enumerateBrsFilesWithBreak(callback: (file: BrsFile) => boolean) {
687
        const files = this.getAllFiles();
258✔
688
        for (const file of files) {
258✔
689
            //only brs files without a typedef
690
            if (isBrsFile(file) && !file.hasTypedef) {
491✔
691
                if (callback(file)) {
381✔
692
                    break;
3✔
693
                }
694
            }
695
        }
696
    }
697

698
    /**
699
     * Call a function for each file directly included in this scope (excluding files found only in parent scopes).
700
     */
701
    public enumerateOwnFiles(callback: (file: BscFile) => void) {
702
        const files = this.getOwnFiles();
7,784✔
703
        for (const file of files) {
7,784✔
704
            //either XML components or files without a typedef
705
            if (isXmlFile(file) || (isBrsFile(file) && !file.hasTypedef)) {
10,597✔
706
                callback(file);
10,574✔
707
            }
708
        }
709
    }
710

711
    /**
712
     * Get the list of callables explicitly defined in files in this scope.
713
     * This excludes ancestor callables
714
     */
715
    public getOwnCallables(): CallableContainer[] {
716
        let result = [] as CallableContainer[];
3,929✔
717
        //get callables from own files
718
        this.enumerateOwnFiles((file) => {
3,929✔
719
            if (isBrsFile(file)) {
4,885✔
720
                for (let callable of file?.callables ?? []) {
4,310!
721
                    result.push({
151,990✔
722
                        callable: callable,
723
                        scope: this
724
                    });
725
                }
726
            }
727
        });
728
        return result;
3,929✔
729
    }
730

731
    /**
732
     * Builds a tree of namespace objects
733
     */
734
    public buildNamespaceLookup() {
735
        let namespaceLookup = new Map<string, ScopeNamespaceContainer>();
6,267✔
736
        this.enumerateBrsFiles((file) => {
6,267✔
737
            const fileNamespaceLookup = file.getNamespaceLookupObject();
7,034✔
738

739
            for (const [lowerNamespaceName, nsContainer] of fileNamespaceLookup) {
7,034✔
740
                if (!namespaceLookup.has(lowerNamespaceName)) {
2,738✔
741
                    const newScopeNsContainer: ScopeNamespaceContainer = {
1,835✔
742
                        namespaceContainers: [],
743
                        symbolTable: new SymbolTable(`Namespace Scope Aggregate: '${nsContainer.fullName}'`),
744
                        firstInstance: nsContainer
745
                    };
746
                    namespaceLookup.set(lowerNamespaceName, newScopeNsContainer);
1,835✔
747
                }
748

749
                const scopeNsContainer = namespaceLookup.get(lowerNamespaceName);
2,738✔
750
                scopeNsContainer.symbolTable.mergeSymbolTable(nsContainer.symbolTable);
2,738✔
751
                scopeNsContainer.namespaceContainers.push(nsContainer);
2,738✔
752
            }
753
        });
754
        return namespaceLookup;
6,267✔
755
    }
756

757
    public getAllNamespaceStatements() {
UNCOV
758
        let result = [] as NamespaceStatement[];
×
759
        this.enumerateBrsFiles((file) => {
×
760
            result.push(...file['_cachedLookups'].namespaceStatements);
×
761
        });
UNCOV
762
        return result;
×
763
    }
764

765
    protected logDebug(...args: any[]) {
766
        this.program.logger.debug(this._debugLogComponentName, ...args);
7,438✔
767
    }
768
    private _debugLogComponentName: string;
769

770

771
    public validationMetrics = {
4,591✔
772
        linkTime: 0,
773
        validationTime: 0
774
    };
775

776
    public shouldValidate(validationOptions: ScopeValidationOptions = { force: false }) {
×
777
        //if this scope is already validated, no need to revalidate
778
        if (this.isValidated === true && !validationOptions.force) {
9,141✔
779
            this.logDebug('validate(): already validated');
3,233✔
780
            return false;
3,233✔
781
        }
782

783
        if (!validationOptions.initialValidation && validationOptions.filesToBeValidatedInScopeContext?.size === 0) {
5,908✔
784
            // There was no need to validate this scope.
785
            (this as any).isValidated = true;
18✔
786
            return false;
18✔
787
        }
788
        return true;
5,890✔
789
    }
790

791

792
    public validate(validationOptions: ScopeValidationOptions = { force: false }) {
2,011✔
793
        this.validationMetrics = {
5,567✔
794
            linkTime: 0,
795
            validationTime: 0
796
        };
797

798
        //if this scope is already validated, no need to revalidate
799
        if (!this.shouldValidate(validationOptions)) {
5,567✔
800
            this.logDebug('validate(): already validated');
1,626✔
801
            // There was no need to validate this scope.
802
            (this as any).isValidated = true;
1,626✔
803
            return false;
1,626✔
804
        }
805

806
        this.useFileCachesForFileLinkLookups = !validationOptions.initialValidation;
3,941✔
807

808
        this.program.logger.time(LogLevel.debug, [this._debugLogComponentName, 'validate()'], () => {
3,941✔
809

810
            let parentScope = this.getParentScope();
3,941✔
811

812
            //validate our parent before we validate ourself
813
            if (parentScope && parentScope.isValidated === false) {
3,941✔
814
                this.logDebug('validate(): validating parent first');
11✔
815
                parentScope.validate(validationOptions);
11✔
816
            }
817

818
            //Since statements from files are shared across multiple scopes, we need to link those statements to the current scope
819

820
            let t0 = performance.now();
3,941✔
821
            this.linkSymbolTable();
3,941✔
822
            this.validationMetrics.linkTime = performance.now() - t0;
3,941✔
823
            const scopeValidateEvent = {
3,941✔
824
                program: this.program,
825
                scope: this,
826
                changedFiles: validationOptions?.changedFiles ?? [],
23,646!
827
                changedSymbols: validationOptions?.changedSymbols
11,823!
828
            };
829
            t0 = performance.now();
3,941✔
830
            this.program.plugins.emit('onScopeValidate', scopeValidateEvent);
3,941✔
831
            this.validationMetrics.validationTime = performance.now() - t0;
3,941✔
832
            //unlink all symbol tables from this scope (so they don't accidentally stick around)
833
            this.unlinkSymbolTable();
3,941✔
834
            (this as any).isValidated = true;
3,941✔
835
        });
836
        for (let file of this.getAllFiles()) {
3,941✔
837
            validationOptions.filesToBeValidatedInScopeContext?.delete(file);
4,911✔
838
        }
839
        return true;
3,941✔
840
    }
841

842
    /**
843
     * Mark this scope as invalid, which means its `validate()` function needs to be called again before use.
844
     */
845
    public invalidate() {
846
        (this as any).isValidated = false;
9,047✔
847
        //clear out various lookups (they'll get regenerated on demand the next time they're requested)
848
        this.cache.clear();
9,047✔
849
    }
850

851
    public get symbolTable(): SymbolTable {
852
        return this.cache.getOrAdd('symbolTable', () => {
1,914,576✔
853
            const result = new SymbolTable(`Scope: '${this.name}'`, () => this.getParentScope()?.symbolTable);
343,894✔
854
            result.addSymbol('m', undefined, new AssociativeArrayType(), SymbolTypeFlag.runtime);
4,806✔
855
            for (let file of this.getOwnFiles()) {
4,806✔
856
                if (isBrsFile(file)) {
4,513✔
857
                    result.mergeSymbolTable(file.parser?.symbolTable);
3,518!
858
                }
859
            }
860
            return result;
4,806✔
861
        });
862
    }
863

864
    /**
865
     * A list of functions that will be called whenever `unlinkSymbolTable` is called
866
     */
867
    private linkSymbolTableDisposables = [];
4,591✔
868

869
    private symbolsAddedDuringLinking: { symbolTable: SymbolTable; name: string; flags: number }[] = [];
4,591✔
870

871
    public get allNamespaceTypeTable() {
872
        return this._allNamespaceTypeTable;
30✔
873
    }
874

875
    private _allNamespaceTypeTable: SymbolTable;
876

877
    /**
878
     * Builds the current symbol table for the scope, by merging the tables for all the files in this scope.
879
     * Also links all file symbols tables to this new table
880
     * This will only rebuilt if the symbol table has not been built before
881
     *
882
     *  Tree of symbol tables:
883
     *  ```
884
     *  Global Scope Symbol Table
885
     *      -  Source Scope Symbol Table :: Aggregate Namespaces Symbol Table (Siblings)
886
     *          - File 1 Symbol Table
887
     *          - File 2 Symbol Table
888
     *      -  Component A Scope Symbol Table :: Aggregate Namespaces Symbol Table (Siblings)
889
     *          - File 1 Symbol Table
890
     *          - File 2 Symbol Table
891
     *      -  Component B Scope Symbol Table :: Aggregate Namespaces Symbol Table (Siblings)
892
     *          - File 1 Symbol Table
893
     *          - File 2 Symbol Table
894
     * ```
895
     */
896
    public linkSymbolTable() {
897
        SymbolTable.cacheVerifier.generateToken();
6,611✔
898
        this._allNamespaceTypeTable = new SymbolTable(`Scope NamespaceTypes ${this.name}`);
6,611✔
899
        for (const file of this.getAllFiles()) {
6,611✔
900
            if (isBrsFile(file)) {
8,667✔
901
                this.linkSymbolTableDisposables.push(
7,409✔
902
                    file.parser.symbolTable.pushParentProvider(() => this.symbolTable)
8,731✔
903
                );
904

905
            }
906
        }
907
        this.enumerateBrsFiles((file) => {
6,611✔
908
            const namespaceTypes = file.getNamespaceSymbolTable();
7,393✔
909

910
            this.linkSymbolTableDisposables.push(
7,393✔
911
                ...this._allNamespaceTypeTable.mergeNamespaceSymbolTables(namespaceTypes)
912
            );
913
        });
914
        for (const [_, scopeNsContainer] of this.namespaceLookup) {
6,611✔
915
            for (let nsContainer of scopeNsContainer.namespaceContainers) {
1,900✔
916
                for (let nsStmt of nsContainer.namespaceStatements) {
2,806✔
917
                    this.linkSymbolTableDisposables.push(
1,733✔
918
                        nsStmt?.getSymbolTable().addSibling(scopeNsContainer.symbolTable)
5,199!
919
                    );
920
                }
921
            }
922
        }
923
        this.linkSymbolTableDisposables.push(
6,611✔
924
            this.symbolTable.addSibling(this._allNamespaceTypeTable)
925
        );
926
    }
927

928
    public unlinkSymbolTable() {
929
        for (const symbolToRemove of this.symbolsAddedDuringLinking) {
6,578✔
UNCOV
930
            this.symbolTable.removeSymbol(symbolToRemove.name);
×
931
        }
932
        this.symbolsAddedDuringLinking = [];
6,578✔
933
        for (const dispose of this.linkSymbolTableDisposables) {
6,578✔
934
            dispose();
17,237✔
935
        }
936
        this.linkSymbolTableDisposables = [];
6,578✔
937

938
        this.cache.delete('namespaceLookup');
6,578✔
939
    }
940

941
    /**
942
     * Get the list of all script imports for this scope
943
     */
944
    public getOwnScriptImports() {
945
        let result = [] as FileReference[];
1,926✔
946
        this.enumerateOwnFiles((file) => {
1,926✔
947
            if (isBrsFile(file)) {
2,843✔
948
                result.push(...file.ownScriptImports);
2,311✔
949
            } else if (isXmlFile(file)) {
532!
950
                result.push(...file.scriptTagImports);
532✔
951
            }
952
        });
953
        return result;
1,926✔
954
    }
955

956

957
    /**
958
     * Find the file with the specified relative path
959
     */
960
    public getFileByRelativePath(relativePath: string) {
961
        if (!relativePath) {
597!
UNCOV
962
            return;
×
963
        }
964
        let files = this.getAllFiles();
597✔
965
        for (let file of files) {
597✔
966
            if (file.destPath.toLowerCase() === relativePath.toLowerCase()) {
991✔
967
                return file;
580✔
968
            }
969
        }
970
    }
971

972
    /**
973
     * Determine if this file is included in this scope (excluding parent scopes)
974
     */
975
    public hasFile(file: BscFile) {
976
        let files = this.getOwnFiles();
59,581✔
977
        let hasFile = files.includes(file);
59,581✔
978
        return hasFile;
59,581✔
979
    }
980

981
    /**
982
     * @param className - The name of the class (including namespace if possible)
983
     * @param callsiteNamespace - the name of the namespace where the call site resides (this is NOT the known namespace of the class).
984
     *                            This is used to help resolve non-namespaced class names that reside in the same namespac as the call site.
985
     */
986
    public getClassHierarchy(className: string, callsiteNamespace?: string) {
987
        let items = [] as FileLink<ClassStatement>[];
16✔
988
        let link = this.getClassFileLink(className, callsiteNamespace);
16✔
989
        while (link) {
16✔
990
            items.push(link);
17✔
991
            link = this.getClassFileLink(link.item.parentClassName?.getName()?.toLowerCase(), callsiteNamespace);
17✔
992
        }
993
        return items;
16✔
994
    }
995
}
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