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

rokucommunity / brighterscript / #13944

24 Feb 2025 01:31PM UTC coverage: 86.72% (-0.04%) from 86.755%
#13944

push

web-flow
Merge 6b46ca8cb into c5c4ff29c

12623 of 15389 branches covered (82.03%)

Branch coverage included in aggregate %.

194 of 209 new or added lines in 7 files covered. (92.82%)

21 existing lines in 3 files now uncovered.

13518 of 14755 relevant lines covered (91.62%)

33631.71 hits per line

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

72.48
/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,314✔
36
        public program: Program,
4,314✔
37
        private _dependencyGraphKey?: string
4,314✔
38
    ) {
39
        this.isValidated = false;
4,314✔
40
        //used for improved logging performance
41
        this._debugLogComponentName = `Scope '${chalk.redBright(this.name)}'`;
4,314✔
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,314✔
51

52
    public get dependencyGraphKey() {
53
        return this._dependencyGraphKey;
8,147✔
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,252✔
63
        for (const file of this.getAllFiles()) {
6,252✔
64
            if (isBrsFile(file) && !file.hasTypedef) {
8,136✔
65
                allFilesValidated = allFilesValidated && file.isValidated;
7,002✔
66
                if (!allFilesValidated) {
7,002✔
67
                    break;
814✔
68
                }
69
            }
70
        }
71
        if (!allFilesValidated) {
6,252✔
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();
814✔
75
        }
76
        return this.cache.getOrAdd('namespaceLookup', () => this.buildNamespaceLookup());
5,438✔
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) {
121
        const nameLower = rootName?.toLowerCase();
×
122
        const lookup = this.namespaceLookup;
×
123
        const lookupKeys = [...lookup.keys()];
×
124
        let lookupName = nameLower;
×
125
        if (containingNamespace) {
×
126
            lookupName = `${containingNamespace?.toLowerCase()}.${nameLower}`;
×
127
        }
128
        const nsList = lookupKeys.filter(key => key === lookupName).map(key => lookup.get(key));
×
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) {
137
        const nameLower = rootName?.toLowerCase();
×
138

139
        let lookupName = nameLower;
×
140
        if (containingNamespace) {
×
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 {
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;
25✔
171
    }
172

173
    private useFileCachesForFileLinkLookups = false;
4,314✔
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();
275!
178
        const itemNameLower = itemName?.toLowerCase();
275!
179
        if (fullNameLower) {
275✔
180
            this.enumerateBrsFilesWithBreak((file) => {
250✔
181
                let stmt = file['_cachedLookups'][cachedMapName].get(fullNameLower);
366✔
182
                if (stmt) {
366!
UNCOV
183
                    result = { item: stmt, file: file };
×
184
                }
185
                return !!stmt;
366✔
186
            });
187
        }
188
        if (!result && itemNameLower && fullNameLower !== itemNameLower) {
275!
UNCOV
189
            this.enumerateBrsFilesWithBreak((file) => {
×
UNCOV
190
                let stmt = file['_cachedLookups'][cachedMapName].get(itemNameLower);
×
UNCOV
191
                if (stmt) {
×
UNCOV
192
                    result = { item: stmt, file: file };
×
193
                }
UNCOV
194
                return !!stmt;
×
195
            });
196
        }
197
        return result;
275✔
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,118✔
207
            return this.getFileLinkFromFileMap('classStatementMap', className, containingNamespace);
179✔
208
        }
209
        const lowerName = className?.toLowerCase();
1,939✔
210
        const fullNameLower = util.getFullyQualifiedClassName(lowerName, containingNamespace)?.toLowerCase();
1,939✔
211
        const classMap = this.getClassMap();
1,939✔
212

213
        let cls = classMap.get(fullNameLower);
1,939✔
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,939✔
216
            cls = classMap.get(lowerName);
6✔
217
        }
218
        return cls;
1,939✔
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!
UNCOV
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!
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,257✔
249
            return this.getFileLinkFromFileMap('enumStatementMap', enumName, containingNamespace);
64✔
250
        }
251
        const lowerName = enumName?.toLowerCase();
1,193✔
252
        const fullNameLower = util.getFullyQualifiedClassName(lowerName, containingNamespace)?.toLowerCase();
1,193✔
253
        const enumMap = this.getEnumMap();
1,193✔
254

255
        let enumeration = enumMap.get(fullNameLower);
1,193✔
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,193✔
258
            enumeration = enumMap.get(lowerName);
65✔
259
        }
260
        return enumeration;
1,193✔
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) {
714✔
294
            return this.getFileLinkFromFileMap('constStatementMap', constName, containingNamespace);
32✔
295
        }
296
        const lowerName = constName?.toLowerCase();
682✔
297
        const fullNameLower = util.getFullyQualifiedClassName(lowerName, containingNamespace)?.toLowerCase();
682✔
298

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

301
        let result = constMap.get(fullNameLower);
682✔
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) {
682✔
304
            result = constMap.get(lowerName);
60✔
305
        }
306
        return result;
682✔
307
    }
308

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

312
        links.push(this.getClassFileLink(name, containingNamespace),
×
313
            this.getInterfaceFileLink(name, containingNamespace),
314
            this.getConstFileLink(name, containingNamespace),
315
            this.getEnumFileLink(name, containingNamespace));
316
        if (includeNameShadowsOutsideNamespace && containingNamespace) {
×
317
            links.push(this.getClassFileLink(name),
×
318
                this.getInterfaceFileLink(name),
319
                this.getConstFileLink(name),
320
                this.getEnumFileLink(name));
321
        }
322
        if (includeNamespaces) {
×
323
            const nameSpaceContainer = this.getFirstNamespaceWithRoot(name, containingNamespace);
×
324
            if (nameSpaceContainer) {
×
325
                links.push({ item: nameSpaceContainer.namespaceStatements?.[0], file: nameSpaceContainer?.file as BrsFile });
×
326
            }
327
        }
328
        const fullNameLower = (containingNamespace ? `${containingNamespace}.${name}` : name).toLowerCase();
×
329
        const callable = this.getCallableByName(name);
×
330
        if (callable) {
×
331
            if ((!callable.hasNamespace && includeNameShadowsOutsideNamespace) || callable.getName(ParseMode.BrighterScript).toLowerCase() === fullNameLower) {
×
332
                // this callable has no namespace, or has same namespace
333
                links.push({ item: callable.functionStatement, file: callable.file as BrsFile });
×
334
            }
335
        }
336
        // remove empty links
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 {
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 {
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 {
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,939✔
390
            const map = new Map<string, FileLink<ClassStatement>>();
829✔
391
            this.enumerateBrsFiles((file) => {
829✔
392
                if (isBrsFile(file)) {
1,017!
393
                    for (let cls of file['_cachedLookups'].classStatements) {
1,017✔
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;
829✔
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,236✔
434
            const map = new Map<string, FileLink<EnumStatement>>();
187✔
435
            this.enumerateBrsFiles((file) => {
187✔
436
                for (let enumStmt of file['_cachedLookups'].enumStatements) {
217✔
437
                    //only track enums with a defined name (i.e. exclude nameless malformed enums)
438
                    if (enumStmt.fullName) {
59!
439
                        map.set(enumStmt.fullName.toLowerCase(), { item: enumStmt, file: file });
59✔
440
                    }
441
                }
442
            });
443
            return map;
187✔
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', () => {
711✔
453
            const map = new Map<string, FileLink<ConstStatement>>();
205✔
454
            this.enumerateBrsFiles((file) => {
205✔
455
                for (let stmt of file['_cachedLookups'].constStatements) {
234✔
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;
205✔
463
        });
464
    }
465

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

471
    /**
472
     * Clean up all event handles
473
     */
474
    public dispose() {
475
        this.unsubscribeFromDependencyGraph?.();
5,378!
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) => {
×
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) {
435,011✔
504
            scope = this.program.globalScope;
7,702✔
505
        }
506
        if (scope) {
435,011✔
507
            return scope;
7,702✔
508
        } else {
509
            //passing null to the cache allows it to skip the factory function in the future
510
            return null;
427,309✔
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;
3,941✔
522
        if (this.unsubscribeFromDependencyGraph) {
3,941✔
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));
3,941✔
528

529
        //invalidate immediately since this is a new scope
530
        this.invalidate();
3,941✔
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();
25,461✔
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', () => {
35,045✔
571
            let result = [] as BscFile[];
5,563✔
572
            let dependencies = this.dependencyGraph.getAllDependencies(this.dependencyGraphKey);
5,563✔
573
            for (let dependency of dependencies) {
5,563✔
574
                //load components by their name
575
                if (dependency.startsWith('component:')) {
7,468✔
576
                    let comp = this.program.getComponent(dependency.replace(/^component:/, ''));
979✔
577
                    if (comp) {
979✔
578
                        result.push(comp.file);
65✔
579
                    }
580
                } else {
581
                    let file = this.program.getFile(dependency);
6,489✔
582
                    if (file) {
6,489✔
583
                        result.push(file);
4,732✔
584
                    }
585
                }
586
            }
587
            return result;
5,563✔
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[] {
595
        return this.cache.getOrAdd('getImmediateFiles', () => {
×
596
            let result = [] as BscFile[];
×
597
            if (isXmlScope(this)) {
×
598
                result.push(this.xmlFile);
×
599
            }
600
            let dependencies = this.dependencyGraph.getImmediateDependencies(this.dependencyGraphKey);
×
601
            for (let dependency of dependencies) {
×
602
                //load components by their name
603
                if (dependency.startsWith('component:')) {
×
604
                    let comp = this.program.getComponent(dependency.replace(/^component:/, ''));
×
605
                    if (comp) {
×
606
                        result.push(...comp.scope.getImmediateFiles());
×
607
                        result.push(comp.file);
×
608
                    }
609
                } else {
610
                    let file = this.program.getFile(dependency);
×
611
                    if (file) {
×
612
                        result.push(file);
×
613
                    }
614
                }
615
            }
616
            this.logDebug('getImmediateFiles', () => result.map(x => x.destPath));
×
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,618✔
627
        if (parentScope) {
3,618✔
628
            return [...this.getOwnCallables(), ...parentScope.getAllCallables()];
1,826✔
629
        } else {
630
            return [...this.getOwnCallables()];
1,792✔
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) {
639
        return this.getCallableMap().get(
×
640
            name.toLowerCase()
641
        );
642
    }
643

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

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

664
            //get a list of all callables, indexed by their lower case names
665
            return util.getCallableContainersByLowerName(callables);
1,775✔
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();
22,212✔
674
        for (const file of files) {
22,212✔
675
            //only brs files without a typedef
676
            if (isBrsFile(file) && !file.hasTypedef) {
30,951✔
677
                callback(file);
25,847✔
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();
250✔
687
        for (const file of files) {
250✔
688
            //only brs files without a typedef
689
            if (isBrsFile(file) && !file.hasTypedef) {
474✔
690
                if (callback(file)) {
366!
UNCOV
691
                    break;
×
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,168✔
702
        for (const file of files) {
7,168✔
703
            //either XML components or files without a typedef
704
            if (isXmlFile(file) || (isBrsFile(file) && !file.hasTypedef)) {
9,756✔
705
                callback(file);
9,736✔
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,621✔
716
        this.logDebug('getOwnCallables() files: ', () => this.getOwnFiles().map(x => x.destPath));
3,621✔
717

718
        //get callables from own files
719
        this.enumerateOwnFiles((file) => {
3,621✔
720
            if (isBrsFile(file)) {
4,503✔
721
                for (let callable of file.callables) {
3,982✔
722
                    result.push({
141,760✔
723
                        callable: callable,
724
                        scope: this
725
                    });
726
                }
727
            }
728
        });
729
        return result;
3,621✔
730
    }
731

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

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

750
                const scopeNsContainer = namespaceLookup.get(lowerNamespaceName);
2,701✔
751
                scopeNsContainer.symbolTable.mergeSymbolTable(nsContainer.symbolTable);
2,701✔
752
                scopeNsContainer.namespaceContainers.push(nsContainer);
2,701✔
753
            }
754
        });
755
        return namespaceLookup;
5,848✔
756
    }
757

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

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

771

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

777
    public validate(validationOptions: ScopeValidationOptions = { force: false }) {
1,884✔
778
        this.validationMetrics = {
5,144✔
779
            linkTime: 0,
780
            validationTime: 0
781
        };
782

783
        //if this scope is already validated, no need to revalidate
784
        if (this.isValidated === true && !validationOptions.force) {
5,144✔
785
            this.logDebug('validate(): already validated');
1,474✔
786
            return false;
1,474✔
787
        }
788

789
        if (!validationOptions.initialValidation && validationOptions.filesToBeValidatedInScopeContext?.size === 0) {
3,670✔
790
            // There was no need to validate this scope.
791
            (this as any).isValidated = true;
10✔
792
            return false;
10✔
793
        }
794

795
        this.useFileCachesForFileLinkLookups = !validationOptions.initialValidation;
3,660✔
796

797
        this.program.logger.time(LogLevel.debug, [this._debugLogComponentName, 'validate()'], () => {
3,660✔
798

799
            let parentScope = this.getParentScope();
3,660✔
800

801
            //validate our parent before we validate ourself
802
            if (parentScope && parentScope.isValidated === false) {
3,660✔
803
                this.logDebug('validate(): validating parent first');
11✔
804
                parentScope.validate(validationOptions);
11✔
805
            }
806

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

809
            let t0 = performance.now();
3,660✔
810
            this.linkSymbolTable();
3,660✔
811
            this.validationMetrics.linkTime = performance.now() - t0;
3,660✔
812
            const scopeValidateEvent = {
3,660✔
813
                program: this.program,
814
                scope: this,
815
                changedFiles: new Array<BscFile>(...(validationOptions?.changedFiles?.values() ?? [])),
32,940!
816
                changedSymbols: validationOptions?.changedSymbols
10,980!
817
            };
818
            t0 = performance.now();
3,660✔
819
            this.program.plugins.emit('beforeScopeValidate', scopeValidateEvent);
3,660✔
820
            this.program.plugins.emit('onScopeValidate', scopeValidateEvent);
3,660✔
821
            this.validationMetrics.validationTime = performance.now() - t0;
3,660✔
822
            this.program.plugins.emit('afterScopeValidate', scopeValidateEvent);
3,660✔
823
            //unlink all symbol tables from this scope (so they don't accidentally stick around)
824
            this.unlinkSymbolTable();
3,660✔
825
            (this as any).isValidated = true;
3,660✔
826
        });
827
        for (let file of this.getAllFiles()) {
3,660✔
828
            validationOptions.filesToBeValidatedInScopeContext?.delete(file);
4,555✔
829
        }
830
        return true;
3,660✔
831
    }
832

833
    /**
834
     * Mark this scope as invalid, which means its `validate()` function needs to be called again before use.
835
     */
836
    public invalidate() {
837
        (this as any).isValidated = false;
8,446✔
838
        //clear out various lookups (they'll get regenerated on demand the next time they're requested)
839
        this.cache.clear();
8,446✔
840
    }
841

842
    public get symbolTable(): SymbolTable {
843
        return this.cache.getOrAdd('symbolTable', () => {
2,076,469✔
844
            const result = new SymbolTable(`Scope: '${this.name}'`, () => this.getParentScope()?.symbolTable);
429,945✔
845
            result.addSymbol('m', undefined, new AssociativeArrayType(), SymbolTypeFlag.runtime);
4,490✔
846
            for (let file of this.getOwnFiles()) {
4,490✔
847
                if (isBrsFile(file)) {
4,223✔
848
                    result.mergeSymbolTable(file.parser?.symbolTable);
3,301!
849
                }
850
            }
851
            return result;
4,490✔
852
        });
853
    }
854

855
    /**
856
     * A list of functions that will be called whenever `unlinkSymbolTable` is called
857
     */
858
    private linkSymbolTableDisposables = [];
4,314✔
859

860
    private symbolsAddedDuringLinking: { symbolTable: SymbolTable; name: string; flags: number }[] = [];
4,314✔
861

862
    public get allNamespaceTypeTable() {
863
        return this._allNamespaceTypeTable;
32✔
864
    }
865

866
    private _allNamespaceTypeTable: SymbolTable;
867

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

896
            }
897
        }
898
        this.enumerateBrsFiles((file) => {
6,179✔
899
            const namespaceTypes = file.getNamespaceSymbolTable();
6,933✔
900

901
            this.linkSymbolTableDisposables.push(
6,933✔
902
                ...this._allNamespaceTypeTable.mergeNamespaceSymbolTables(namespaceTypes)
903
            );
904
        });
905
        for (const [_, scopeNsContainer] of this.namespaceLookup) {
6,179✔
906
            for (let nsContainer of scopeNsContainer.namespaceContainers) {
1,862✔
907
                for (let nsStmt of nsContainer.namespaceStatements) {
2,766✔
908
                    this.linkSymbolTableDisposables.push(
1,703✔
909
                        nsStmt?.getSymbolTable().addSibling(scopeNsContainer.symbolTable)
5,109!
910
                    );
911
                }
912
            }
913
        }
914
        this.linkSymbolTableDisposables.push(
6,179✔
915
            this.symbolTable.addSibling(this._allNamespaceTypeTable)
916
        );
917
    }
918

919
    public unlinkSymbolTable() {
920
        for (const symbolToRemove of this.symbolsAddedDuringLinking) {
6,150✔
921
            this.symbolTable.removeSymbol(symbolToRemove.name);
×
922
        }
923
        this.symbolsAddedDuringLinking = [];
6,150✔
924
        for (const dispose of this.linkSymbolTableDisposables) {
6,150✔
925
            dispose();
16,294✔
926
        }
927
        this.linkSymbolTableDisposables = [];
6,150✔
928

929
        this.cache.delete('namespaceLookup');
6,150✔
930
    }
931

932
    /**
933
     * Get the list of all script imports for this scope
934
     */
935
    public getOwnScriptImports() {
936
        let result = [] as FileReference[];
1,772✔
937
        this.enumerateOwnFiles((file) => {
1,772✔
938
            if (isBrsFile(file)) {
2,615✔
939
                result.push(...file.ownScriptImports);
2,137✔
940
            } else if (isXmlFile(file)) {
478!
941
                result.push(...file.scriptTagImports);
478✔
942
            }
943
        });
944
        return result;
1,772✔
945
    }
946

947

948
    /**
949
     * Find the file with the specified relative path
950
     */
951
    public getFileByRelativePath(relativePath: string) {
952
        if (!relativePath) {
540!
953
            return;
×
954
        }
955
        let files = this.getAllFiles();
540✔
956
        for (let file of files) {
540✔
957
            if (file.destPath.toLowerCase() === relativePath.toLowerCase()) {
909✔
958
                return file;
524✔
959
            }
960
        }
961
    }
962

963
    /**
964
     * Determine if this file is included in this scope (excluding parent scopes)
965
     */
966
    public hasFile(file: BscFile) {
967
        let files = this.getOwnFiles();
58,628✔
968
        let hasFile = files.includes(file);
58,628✔
969
        return hasFile;
58,628✔
970
    }
971

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