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

rokucommunity / brighterscript / #14385

09 May 2025 11:44AM UTC coverage: 87.032% (-2.0%) from 89.017%
#14385

push

web-flow
Merge a194c3925 into 489231ac7

13732 of 16677 branches covered (82.34%)

Branch coverage included in aggregate %.

8175 of 8874 new or added lines in 103 files covered. (92.12%)

84 existing lines in 22 files now uncovered.

14604 of 15881 relevant lines covered (91.96%)

20324.31 hits per line

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

72.95
/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

23
/**
24
 * Assign some few factories to the SymbolTable to prevent cyclical imports. This file seems like the most intuitive place to do the linking
25
 * since Scope will be used by pretty much everything
26
 */
27
SymbolTable.referenceTypeFactory = referenceTypeFactory;
1✔
28
SymbolTable.unionTypeFactory = unionTypeFactory;
1✔
29

30
/**
31
 * A class to keep track of all declarations within a given scope (like source scope, component scope)
32
 */
33
export class Scope {
1✔
34
    constructor(
35
        public name: string,
4,573✔
36
        public program: Program,
4,573✔
37
        private _dependencyGraphKey?: string
4,573✔
38
    ) {
39
        this.isValidated = false;
4,573✔
40
        //used for improved logging performance
41
        this._debugLogComponentName = `Scope '${chalk.redBright(this.name)}'`;
4,573✔
42
    }
43

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

50
    protected cache = new Cache();
4,573✔
51

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

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

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

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

98

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

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

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

132

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

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

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

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

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

173
    private useFileCachesForFileLinkLookups = false;
4,573✔
174

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

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

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

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

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

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

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

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

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

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

299
        const constMap = this.getConstMap();
718✔
300

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

533
    /**
534
     * Get the file from this scope with the given path.
535
     * @param filePath can be a srcPath or destPath
536
     * @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
537
     */
538
    public getFile<TFile extends BscFile>(filePath: string, normalizePath = true) {
11✔
539
        if (typeof filePath !== 'string') {
11!
540
            return undefined;
×
541
        }
542

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

769

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

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

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

790

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

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

805
        this.useFileCachesForFileLinkLookups = !validationOptions.initialValidation;
3,923✔
806

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

809
            let parentScope = this.getParentScope();
3,923✔
810

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

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

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

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

850
    public get symbolTable(): SymbolTable {
851
        return this.cache.getOrAdd('symbolTable', () => {
1,905,974✔
852
            const result = new SymbolTable(`Scope: '${this.name}'`, () => this.getParentScope()?.symbolTable);
342,494✔
853
            result.addSymbol('m', undefined, new AssociativeArrayType(), SymbolTypeFlag.runtime);
4,788✔
854
            for (let file of this.getOwnFiles()) {
4,788✔
855
                if (isBrsFile(file)) {
4,504✔
856
                    result.mergeSymbolTable(file.parser?.symbolTable);
3,509!
857
                }
858
            }
859
            return result;
4,788✔
860
        });
861
    }
862

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

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

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

874
    private _allNamespaceTypeTable: SymbolTable;
875

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

904
            }
905
        }
906
        this.enumerateBrsFiles((file) => {
6,580✔
907
            const namespaceTypes = file.getNamespaceSymbolTable();
7,361✔
908

909
            this.linkSymbolTableDisposables.push(
7,361✔
910
                ...this._allNamespaceTypeTable.mergeNamespaceSymbolTables(namespaceTypes)
911
            );
912
        });
913
        for (const [_, scopeNsContainer] of this.namespaceLookup) {
6,580✔
914
            for (let nsContainer of scopeNsContainer.namespaceContainers) {
1,894✔
915
                for (let nsStmt of nsContainer.namespaceStatements) {
2,798✔
916
                    this.linkSymbolTableDisposables.push(
1,729✔
917
                        nsStmt?.getSymbolTable().addSibling(scopeNsContainer.symbolTable)
5,187!
918
                    );
919
                }
920
            }
921
        }
922
        this.linkSymbolTableDisposables.push(
6,580✔
923
            this.symbolTable.addSibling(this._allNamespaceTypeTable)
924
        );
925
    }
926

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

937
        this.cache.delete('namespaceLookup');
6,549✔
938
    }
939

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

955

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

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

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