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

rokucommunity / brighterscript / #13071

25 Sep 2024 04:16PM UTC coverage: 86.525% (-1.4%) from 87.933%
#13071

push

web-flow
Merge c610b9e4e into 56dcaaa63

10903 of 13389 branches covered (81.43%)

Branch coverage included in aggregate %.

6936 of 7533 new or added lines in 100 files covered. (92.07%)

83 existing lines in 18 files now uncovered.

12548 of 13714 relevant lines covered (91.5%)

27591.0 hits per line

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

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

52
    public get dependencyGraphKey() {
53
        return this._dependencyGraphKey;
5,782✔
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;
4,157✔
63
        for (const file of this.getAllFiles()) {
4,157✔
64
            if (isBrsFile(file) && !file.hasTypedef) {
5,151✔
65
                allFilesValidated = allFilesValidated && file.isValidated;
4,622✔
66
                if (!allFilesValidated) {
4,622✔
67
                    break;
73✔
68
                }
69
            }
70
        }
71
        if (!allFilesValidated) {
4,157✔
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();
73✔
75
        }
76
        return this.cache.getOrAdd('namespaceLookup', () => this.buildNamespaceLookup());
4,084✔
77
    }
78

79

80
    /**
81
     * Get a NamespaceContainer by its name, looking for a fully qualified version first, then global version next if not found
82
     */
83
    public getNamespace(name: string, containingNamespace?: string) {
84
        const nameLower = name?.toLowerCase();
50!
85
        const lookup = this.namespaceLookup;
50✔
86

87
        let ns: NamespaceContainer;
88
        if (containingNamespace) {
50!
UNCOV
89
            ns = lookup.get(`${containingNamespace?.toLowerCase()}.${nameLower}`);
×
90
        }
91
        //if we couldn't find the namespace by its full namespaced name, look for a global version
92
        if (!ns) {
50!
93
            ns = lookup.get(nameLower);
50✔
94
        }
95
        return ns;
50✔
96
    }
97

98
    /**
99
     * Get a NamespaceContainer by its name, looking for a fully qualified version first, then global version next if not found
100
     */
101
    public getNamespacesWithRoot(rootName: string, containingNamespace?: string) {
NEW
102
        const nameLower = rootName?.toLowerCase();
×
NEW
103
        const lookup = this.namespaceLookup;
×
NEW
104
        const lookupKeys = [...lookup.keys()];
×
NEW
105
        let lookupName = nameLower;
×
NEW
106
        if (containingNamespace) {
×
NEW
107
            lookupName = `${containingNamespace?.toLowerCase()}.${nameLower}`;
×
108
        }
NEW
109
        const nsList = lookupKeys.filter(key => key === lookupName).map(key => lookup.get(key));
×
NEW
110
        return nsList;
×
111
    }
112

113

114
    /**
115
     * Get a NamespaceContainer by its name, looking for a fully qualified version first, then global version next if not found
116
     */
117
    public getFirstNamespaceWithRoot(rootName: string, containingNamespace?: string) {
NEW
118
        const nameLower = rootName?.toLowerCase();
×
119

NEW
120
        let lookupName = nameLower;
×
NEW
121
        if (containingNamespace) {
×
NEW
122
            lookupName = `${containingNamespace?.toLowerCase()}.${nameLower}`;
×
123
        }
NEW
124
        return this.namespaceLookup.get(lookupName);
×
125
    }
126

127
    /**
128
     * Get the class with the specified name.
129
     * @param className - The class name, including the namespace of the class if possible
130
     * @param containingNamespace - The namespace used to resolve relative class names. (i.e. the namespace around the current statement trying to find a class)
131
     */
132
    public getClass(className: string, containingNamespace?: string): ClassStatement {
133
        return this.getClassFileLink(className, containingNamespace)?.item;
8!
134
    }
135

136
    /**
137
     * Get the interface with the specified name.
138
     * @param ifaceName - The interface name, including the namespace of the interface if possible
139
     * @param containingNamespace - The namespace used to resolve relative interface names. (i.e. the namespace around the current statement trying to find a interface)
140
     */
141
    public getInterface(ifaceName: string, containingNamespace?: string): InterfaceStatement {
UNCOV
142
        return this.getInterfaceFileLink(ifaceName, containingNamespace)?.item;
×
143
    }
144

145
    /**
146
     * Get the enum with the specified name.
147
     * @param enumName - The enum name, including the namespace if possible
148
     * @param containingNamespace - The namespace used to resolve relative enum names. (i.e. the namespace around the current statement trying to find an enum)
149
     */
150
    public getEnum(enumName: string, containingNamespace?: string): EnumStatement {
151
        return this.getEnumFileLink(enumName, containingNamespace)?.item;
15✔
152
    }
153

154
    private useFileCachesForFileLinkLookups = false;
3,341✔
155

156
    private getFileLinkFromFileMap<T>(cachedMapName: string, itemName: string, containingNamespace?: string): FileLink<T> {
157
        let result: FileLink<T>;
158
        const fullNameLower = util.getFullyQualifiedClassName(itemName, containingNamespace)?.toLowerCase();
3,449✔
159
        const itemNameLower = itemName?.toLowerCase();
3,449✔
160
        if (fullNameLower) {
3,449✔
161
            this.enumerateBrsFilesWithBreak((file) => {
3,125✔
162
                let stmt = file['_cachedLookups'][cachedMapName].get(fullNameLower);
3,500✔
163
                if (stmt) {
3,500✔
164
                    result = { item: stmt, file: file };
335✔
165
                }
166
                return !!stmt;
3,500✔
167
            });
168
        }
169
        if (!result && itemNameLower && fullNameLower !== itemNameLower) {
3,449✔
170
            this.enumerateBrsFilesWithBreak((file) => {
86✔
171
                let stmt = file['_cachedLookups'][cachedMapName].get(itemNameLower);
101✔
172
                if (stmt) {
101✔
173
                    result = { item: stmt, file: file };
5✔
174
                }
175
                return !!stmt;
101✔
176
            });
177
        }
178
        return result;
3,449✔
179
    }
180

181
    /**
182
     * Get a class and its containing file by the class name
183
     * @param className - The class name, including the namespace of the class if possible
184
     * @param containingNamespace - The namespace used to resolve relative class names. (i.e. the namespace around the current statement trying to find a class)
185
     */
186
    public getClassFileLink(className: string, containingNamespace?: string): FileLink<ClassStatement> {
187
        if (this.useFileCachesForFileLinkLookups) {
1,732✔
188
            return this.getFileLinkFromFileMap('classStatementMap', className, containingNamespace);
1,714✔
189
        }
190
        const lowerName = className?.toLowerCase();
18!
191
        const fullNameLower = util.getFullyQualifiedClassName(lowerName, containingNamespace)?.toLowerCase();
18!
192
        const classMap = this.getClassMap();
18✔
193

194
        let cls = classMap.get(fullNameLower);
18✔
195
        //if we couldn't find the class by its full namespaced name, look for a global class with that name
196
        if (!cls && lowerName && lowerName !== fullNameLower) {
18!
NEW
197
            cls = classMap.get(lowerName);
×
198
        }
199
        return cls;
18✔
200
    }
201

202
    /**
203
     * Get an interface and its containing file by the interface name
204
     * @param ifaceName - The interface name, including the namespace of the interface if possible
205
     * @param containingNamespace - The namespace used to resolve relative interface names. (i.e. the namespace around the current statement trying to find a interface)
206
     */
207
    public getInterfaceFileLink(ifaceName: string, containingNamespace?: string): FileLink<InterfaceStatement> {
208
        if (this.useFileCachesForFileLinkLookups) {
8✔
209
            return this.getFileLinkFromFileMap('interfaceStatementMap', ifaceName, containingNamespace);
5✔
210
        }
211
        const lowerName = ifaceName?.toLowerCase();
3!
212
        const fullNameLower = util.getFullyQualifiedClassName(lowerName, containingNamespace)?.toLowerCase();
3!
213
        const ifaceMap = this.getInterfaceMap();
3✔
214

215
        let iface = ifaceMap.get(fullNameLower);
3✔
216
        //if we couldn't find the iface by its full namespaced name, look for a global class with that name
217
        if (!iface && lowerName && lowerName !== fullNameLower) {
3!
UNCOV
218
            iface = ifaceMap.get(lowerName);
×
219
        }
220
        return iface;
3✔
221
    }
222

223
    /**
224
     * Get an Enum and its containing file by the Enum name
225
     * @param enumName - The Enum name, including the namespace of the enum if possible
226
     * @param containingNamespace - The namespace used to resolve relative enum names. (i.e. the namespace around the current statement trying to find a enum)
227
     */
228
    public getEnumFileLink(enumName: string, containingNamespace?: string): FileLink<EnumStatement> {
229
        if (this.useFileCachesForFileLinkLookups) {
1,132✔
230
            return this.getFileLinkFromFileMap('enumStatementMap', enumName, containingNamespace);
1,100✔
231
        }
232
        const lowerName = enumName?.toLowerCase();
32!
233
        const fullNameLower = util.getFullyQualifiedClassName(lowerName, containingNamespace)?.toLowerCase();
32!
234
        const enumMap = this.getEnumMap();
32✔
235

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

244
    /**
245
     * Get an Enum and its containing file by the Enum name
246
     * @param enumMemberName - The Enum name, including the namespace of the enum if possible
247
     * @param containingNamespace - The namespace used to resolve relative enum names. (i.e. the namespace around the current statement trying to find a enum)
248
     */
249
    public getEnumMemberFileLink(enumMemberName: string, containingNamespace?: string): FileLink<EnumMemberStatement> {
250
        let lowerNameParts = enumMemberName?.toLowerCase()?.split('.');
10✔
251
        let memberName = lowerNameParts?.splice(lowerNameParts.length - 1, 1)?.[0];
10✔
252
        let lowerName = lowerNameParts?.join('.').toLowerCase();
10✔
253
        const enumMap = this.getEnumMap();
10✔
254

255
        let enumeration = enumMap.get(
10✔
256
            util.getFullyQualifiedClassName(lowerName, containingNamespace?.toLowerCase())
30!
257
        );
258
        //if we couldn't find the enum by its full namespaced name, look for a global enum with that name
259
        if (!enumeration) {
10✔
260
            enumeration = enumMap.get(lowerName);
9✔
261
        }
262
        if (enumeration) {
10✔
263
            let member = enumeration.item.findChild<EnumMemberStatement>((child) => isEnumMemberStatement(child) && child.name?.toLowerCase() === memberName);
1!
264
            return member ? { item: member, file: enumeration.file } : undefined;
1!
265
        }
266
    }
267

268
    /**
269
     * Get a constant and its containing file by the constant name
270
     * @param constName - The constant name, including the namespace of the constant if possible
271
     * @param containingNamespace - The namespace used to resolve relative constant names. (i.e. the namespace around the current statement trying to find a constant)
272
     */
273
    public getConstFileLink(constName: string, containingNamespace?: string): FileLink<ConstStatement> {
274
        if (this.useFileCachesForFileLinkLookups) {
655✔
275
            return this.getFileLinkFromFileMap('constStatementMap', constName, containingNamespace);
630✔
276
        }
277
        const lowerName = constName?.toLowerCase();
25✔
278
        const fullNameLower = util.getFullyQualifiedClassName(lowerName, containingNamespace)?.toLowerCase();
25✔
279

280
        const constMap = this.getConstMap();
25✔
281

282
        let result = constMap.get(fullNameLower);
25✔
283
        //if we couldn't find the constant by its full namespaced name, look for a global constant with that name
284
        if (!result && lowerName !== fullNameLower) {
25!
UNCOV
285
            result = constMap.get(lowerName);
×
286
        }
287
        return result;
25✔
288
    }
289

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

NEW
293
        links.push(this.getClassFileLink(name, containingNamespace),
×
294
            this.getInterfaceFileLink(name, containingNamespace),
295
            this.getConstFileLink(name, containingNamespace),
296
            this.getEnumFileLink(name, containingNamespace));
NEW
297
        if (includeNameShadowsOutsideNamespace && containingNamespace) {
×
NEW
298
            links.push(this.getClassFileLink(name),
×
299
                this.getInterfaceFileLink(name),
300
                this.getConstFileLink(name),
301
                this.getEnumFileLink(name));
302
        }
NEW
303
        if (includeNamespaces) {
×
NEW
304
            const nameSpaceContainer = this.getFirstNamespaceWithRoot(name, containingNamespace);
×
NEW
305
            if (nameSpaceContainer) {
×
NEW
306
                links.push({ item: nameSpaceContainer.namespaceStatements?.[0], file: nameSpaceContainer?.file as BrsFile });
×
307
            }
308
        }
NEW
309
        const fullNameLower = (containingNamespace ? `${containingNamespace}.${name}` : name).toLowerCase();
×
NEW
310
        const callable = this.getCallableByName(name);
×
NEW
311
        if (callable) {
×
NEW
312
            if ((!callable.hasNamespace && includeNameShadowsOutsideNamespace) || callable.getName(ParseMode.BrighterScript).toLowerCase() === fullNameLower) {
×
313
                // this callable has no namespace, or has same namespace
NEW
314
                links.push({ item: callable.functionStatement, file: callable.file as BrsFile });
×
315
            }
316
        }
317
        // remove empty links
NEW
318
        return links.filter(link => link);
×
319
    }
320

321
    /**
322
     * Get a map of all enums by their member name.
323
     * The keys are lower-case fully-qualified paths to the enum and its member. For example:
324
     * namespace.enum.value
325
     */
326
    public getEnumMemberMap() {
327
        return this.cache.getOrAdd('enumMemberMap', () => {
×
328
            const result = new Map<string, EnumMemberStatement>();
×
329
            for (const [key, eenum] of this.getEnumMap()) {
×
330
                for (const member of eenum.item.getMembers()) {
×
331
                    result.set(`${key}.${member.name.toLowerCase()}`, member);
×
332
                }
333
            }
334
            return result;
×
335
        });
336
    }
337

338
    /**
339
     * Tests if a class exists with the specified name
340
     * @param className - the all-lower-case namespace-included class name
341
     * @param namespaceName - The namespace used to resolve relative class names. (i.e. the namespace around the current statement trying to find a class)
342
     */
343
    public hasClass(className: string, namespaceName?: string): boolean {
UNCOV
344
        return !!this.getClass(className, namespaceName);
×
345
    }
346

347
    /**
348
     * Tests if an interface exists with the specified name
349
     * @param ifaceName - the all-lower-case namespace-included interface name
350
     * @param namespaceName - the current namespace name
351
     */
352
    public hasInterface(ifaceName: string, namespaceName?: string): boolean {
UNCOV
353
        return !!this.getInterface(ifaceName, namespaceName);
×
354
    }
355

356
    /**
357
     * Tests if an enum exists with the specified name
358
     * @param enumName - the all-lower-case namespace-included enum name
359
     * @param namespaceName - the current namespace name
360
     */
361
    public hasEnum(enumName: string, namespaceName?: string): boolean {
UNCOV
362
        return !!this.getEnum(enumName, namespaceName);
×
363
    }
364

365
    /**
366
     * A dictionary of all classes in this scope. This includes namespaced classes always with their full name.
367
     * The key is stored in lower case
368
     */
369
    public getClassMap(): Map<string, FileLink<ClassStatement>> {
370
        return this.cache.getOrAdd('classMap', () => {
18✔
371
            const map = new Map<string, FileLink<ClassStatement>>();
6✔
372
            this.enumerateBrsFiles((file) => {
6✔
373
                if (isBrsFile(file)) {
9!
374
                    for (let cls of file['_cachedLookups'].classStatements) {
9✔
375
                        const className = cls.getName(ParseMode.BrighterScript);
6✔
376
                        //only track classes with a defined name (i.e. exclude nameless malformed classes)
377
                        if (className) {
6!
378
                            map.set(className.toLowerCase(), { item: cls, file: file });
6✔
379
                        }
380
                    }
381
                }
382
            });
383
            return map;
6✔
384
        });
385
    }
386

387
    /**
388
     * A dictionary of all Interfaces in this scope. This includes namespaced Interfaces always with their full name.
389
     * The key is stored in lower case
390
     */
391
    public getInterfaceMap(): Map<string, FileLink<InterfaceStatement>> {
392
        return this.cache.getOrAdd('interfaceMap', () => {
3✔
393
            const map = new Map<string, FileLink<InterfaceStatement>>();
3✔
394
            this.enumerateBrsFiles((file) => {
3✔
395
                if (isBrsFile(file)) {
6!
396
                    for (let iface of file['_cachedLookups'].interfaceStatements) {
6✔
NEW
397
                        const ifaceName = iface.getName(ParseMode.BrighterScript);
×
398
                        //only track classes with a defined name (i.e. exclude nameless malformed classes)
NEW
399
                        if (ifaceName) {
×
NEW
400
                            map.set(ifaceName.toLowerCase(), { item: iface, file: file });
×
401
                        }
402
                    }
403
                }
404
            });
405
            return map;
3✔
406
        });
407
    }
408

409
    /**
410
     * A dictionary of all enums in this scope. This includes namespaced enums always with their full name.
411
     * The key is stored in lower case
412
     */
413
    public getEnumMap(): Map<string, FileLink<EnumStatement>> {
414
        return this.cache.getOrAdd('enumMap', () => {
71✔
415
            const map = new Map<string, FileLink<EnumStatement>>();
34✔
416
            this.enumerateBrsFiles((file) => {
34✔
417
                for (let enumStmt of file['_cachedLookups'].enumStatements) {
41✔
418
                    //only track enums with a defined name (i.e. exclude nameless malformed enums)
419
                    if (enumStmt.fullName) {
9!
420
                        map.set(enumStmt.fullName.toLowerCase(), { item: enumStmt, file: file });
9✔
421
                    }
422
                }
423
            });
424
            return map;
34✔
425
        });
426
    }
427

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

447
    protected onDependenciesChanged(event: DependencyChangedEvent) {
448
        this.logDebug('invalidated because dependency graph said [', event.sourceKey, '] changed');
2,203✔
449
        this.invalidate();
2,203✔
450
    }
451

452
    /**
453
     * Clean up all event handles
454
     */
455
    public dispose() {
456
        this.unsubscribeFromDependencyGraph?.();
4,543!
457
    }
458

459
    /**
460
     * Does this scope know about the given namespace name?
461
     * @param namespaceName - the name of the namespace (i.e. "NameA", or "NameA.NameB", etc...)
462
     */
463
    public isKnownNamespace(namespaceName: string) {
464
        let namespaceNameLower = namespaceName.toLowerCase();
×
465
        this.enumerateBrsFiles((file) => {
×
NEW
466
            for (let namespace of file['_cachedLookups'].namespaceStatements) {
×
467
                let loopNamespaceNameLower = namespace.name.toLowerCase();
×
468
                if (loopNamespaceNameLower === namespaceNameLower || loopNamespaceNameLower.startsWith(namespaceNameLower + '.')) {
×
469
                    return true;
×
470
                }
471
            }
472
        });
473
        return false;
×
474
    }
475

476
    /**
477
     * Get the parent scope for this scope (for source scope this will always be the globalScope).
478
     * XmlScope overrides this to return the parent xml scope if available.
479
     * For globalScope this will return null.
480
     */
481
    public getParentScope(): Scope | null {
482
        let scope: Scope | undefined;
483
        //use the global scope if we didn't find a sope and this is not the global scope
484
        if (this.program.globalScope !== this) {
342,423✔
485
            scope = this.program.globalScope;
5,023✔
486
        }
487
        if (scope) {
342,423✔
488
            return scope;
5,023✔
489
        } else {
490
            //passing null to the cache allows it to skip the factory function in the future
491
            return null;
337,400✔
492
        }
493
    }
494

495
    private dependencyGraph: DependencyGraph;
496
    /**
497
     * An unsubscribe function for the dependencyGraph subscription
498
     */
499
    private unsubscribeFromDependencyGraph: () => void;
500

501
    public attachDependencyGraph(dependencyGraph: DependencyGraph) {
502
        this.dependencyGraph = dependencyGraph;
3,343✔
503
        if (this.unsubscribeFromDependencyGraph) {
3,343✔
504
            this.unsubscribeFromDependencyGraph();
2✔
505
        }
506

507
        //anytime a dependency for this scope changes, we need to be revalidated
508
        this.unsubscribeFromDependencyGraph = this.dependencyGraph.onchange(this.dependencyGraphKey, this.onDependenciesChanged.bind(this));
3,343✔
509

510
        //invalidate immediately since this is a new scope
511
        this.invalidate();
3,343✔
512
    }
513

514
    /**
515
     * Get the file from this scope with the given path.
516
     * @param filePath can be a srcPath or destPath
517
     * @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
518
     */
519
    public getFile<TFile extends BscFile>(filePath: string, normalizePath = true) {
11✔
520
        if (typeof filePath !== 'string') {
11!
521
            return undefined;
×
522
        }
523

524
        const key: keyof Pick<BscFile, 'srcPath' | 'destPath'> = path.isAbsolute(filePath) ? 'srcPath' : 'destPath';
11✔
525
        let map = this.cache.getOrAdd('fileMaps-srcPath', () => {
11✔
526
            const result = new Map<string, BscFile>();
7✔
527
            for (const file of this.getAllFiles()) {
7✔
528
                result.set(file[key].toLowerCase(), file);
11✔
529
            }
530
            return result;
7✔
531
        });
532
        return map.get(
11✔
533
            (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase()
11!
534
        ) as TFile;
535
    }
536

537
    /**
538
     * Get the list of files referenced by this scope that are actually loaded in the program.
539
     * Excludes files from ancestor scopes
540
     */
541
    public getOwnFiles() {
542
        //source scope only inherits files from global, so just return all files. This function mostly exists to assist XmlScope
543
        return this.getAllFiles();
16,769✔
544
    }
545

546
    /**
547
     * Get the list of files referenced by this scope that are actually loaded in the program.
548
     * Includes files from this scope and all ancestor scopes
549
     */
550
    public getAllFiles(): BscFile[] {
551
        return this.cache.getOrAdd('getAllFiles', () => {
28,508✔
552
            let result = [] as BscFile[];
3,286✔
553
            let dependencies = this.dependencyGraph.getAllDependencies(this.dependencyGraphKey);
3,286✔
554
            for (let dependency of dependencies) {
3,286✔
555
                //load components by their name
556
                if (dependency.startsWith('component:')) {
3,490✔
557
                    let comp = this.program.getComponent(dependency.replace(/^component:/, ''));
486✔
558
                    if (comp) {
486✔
559
                        result.push(comp.file);
34✔
560
                    }
561
                } else {
562
                    let file = this.program.getFile(dependency);
3,004✔
563
                    if (file) {
3,004✔
564
                        result.push(file);
2,099✔
565
                    }
566
                }
567
            }
568
            this.logDebug('getAllFiles', () => result.map(x => x.destPath));
3,286✔
569
            return result;
3,286✔
570
        });
571
    }
572

573
    /**
574
     * Gets a list of all files in this scope, but not imported files, and not from ancestor scopes
575
     */
576
    public getImmediateFiles(): BscFile[] {
NEW
577
        return this.cache.getOrAdd('getImmediateFiles', () => {
×
NEW
578
            let result = [] as BscFile[];
×
NEW
579
            if (isXmlScope(this)) {
×
NEW
580
                result.push(this.xmlFile);
×
581
            }
NEW
582
            let dependencies = this.dependencyGraph.getImmediateDependencies(this.dependencyGraphKey);
×
NEW
583
            for (let dependency of dependencies) {
×
584
                //load components by their name
NEW
585
                if (dependency.startsWith('component:')) {
×
NEW
586
                    let comp = this.program.getComponent(dependency.replace(/^component:/, ''));
×
NEW
587
                    if (comp) {
×
NEW
588
                        result.push(...comp.scope.getImmediateFiles());
×
NEW
589
                        result.push(comp.file);
×
590
                    }
591
                } else {
NEW
592
                    let file = this.program.getFile(dependency);
×
NEW
593
                    if (file) {
×
NEW
594
                        result.push(file);
×
595
                    }
596
                }
597
            }
NEW
598
            this.logDebug('getImmediateFiles', () => result.map(x => x.destPath));
×
NEW
599
            return result;
×
600
        });
601
    }
602

603
    /**
604
     * Get the list of callables available in this scope (either declared in this scope or in a parent scope)
605
     */
606
    public getAllCallables(): CallableContainer[] {
607
        //get callables from parent scopes
608
        let parentScope = this.getParentScope();
3,258✔
609
        if (parentScope) {
3,258✔
610
            return [...this.getOwnCallables(), ...parentScope.getAllCallables()];
1,646✔
611
        } else {
612
            return [...this.getOwnCallables()];
1,612✔
613
        }
614
    }
615

616
    /**
617
     * Get the callable with the specified name.
618
     * If there are overridden callables with the same name, the closest callable to this scope is returned
619
     */
620
    public getCallableByName(name: string) {
UNCOV
621
        return this.getCallableMap().get(
×
622
            name.toLowerCase()
623
        );
624
    }
625

626
    public getCallableMap() {
UNCOV
627
        return this.cache.getOrAdd('callableMap', () => {
×
UNCOV
628
            const result = new Map<string, Callable>();
×
UNCOV
629
            for (let callable of this.getAllCallables()) {
×
UNCOV
630
                const callableName = callable.callable.getName(ParseMode.BrighterScript)?.toLowerCase();
×
UNCOV
631
                result.set(callableName, callable.callable);
×
UNCOV
632
                result.set(
×
633
                    // Split by `.` and check the last term to consider namespaces.
634
                    callableName.split('.').pop()?.toLowerCase(),
×
635
                    callable.callable
636
                );
637
            }
UNCOV
638
            return result;
×
639
        });
640
    }
641

642
    public getCallableContainerMap() {
643
        return this.cache.getOrAdd('callableContainerMap', () => {
3,284✔
644
            let callables = this.getAllCallables();
1,595✔
645

646
            //get a list of all callables, indexed by their lower case names
647
            return util.getCallableContainersByLowerName(callables);
1,595✔
648
        });
649
    }
650

651
    /**
652
     * Iterate over Brs files not shadowed by typedefs
653
     */
654
    public enumerateBrsFiles(callback: (file: BrsFile) => void) {
655
        const files = this.getAllFiles();
15,441✔
656
        for (const file of files) {
15,441✔
657
            //only brs files without a typedef
658
            if (isBrsFile(file) && !file.hasTypedef) {
21,674✔
659
                callback(file);
18,242✔
660
            }
661
        }
662
    }
663

664
    /**
665
     * Iterate over Brs files not shadowed by typedefs
666
     */
667
    public enumerateBrsFilesWithBreak(callback: (file: BrsFile) => boolean) {
668
        const files = this.getAllFiles();
3,211✔
669
        for (const file of files) {
3,211✔
670
            //only brs files without a typedef
671
            if (isBrsFile(file) && !file.hasTypedef) {
3,798✔
672
                if (callback(file)) {
3,601✔
673
                    break;
340✔
674
                }
675
            }
676
        }
677
    }
678

679
    /**
680
     * Call a function for each file directly included in this scope (excluding files found only in parent scopes).
681
     */
682
    public enumerateOwnFiles(callback: (file: BscFile) => void) {
683
        const files = this.getOwnFiles();
6,451✔
684
        for (const file of files) {
6,451✔
685
            //either XML components or files without a typedef
686
            if (isXmlFile(file) || (isBrsFile(file) && !file.hasTypedef)) {
8,904✔
687
                callback(file);
8,884✔
688
            }
689
        }
690
    }
691

692
    /**
693
     * Get the list of callables explicitly defined in files in this scope.
694
     * This excludes ancestor callables
695
     */
696
    public getOwnCallables(): CallableContainer[] {
697
        let result = [] as CallableContainer[];
3,261✔
698
        this.logDebug('getOwnCallables() files: ', () => this.getOwnFiles().map(x => x.destPath));
3,261✔
699

700
        //get callables from own files
701
        this.enumerateOwnFiles((file) => {
3,261✔
702
            if (isBrsFile(file)) {
4,098✔
703
                for (let callable of file.callables) {
3,626✔
704
                    result.push({
127,513✔
705
                        callable: callable,
706
                        scope: this
707
                    });
708
                }
709
            }
710
        });
711
        return result;
3,261✔
712
    }
713

714
    /**
715
     * Builds a tree of namespace objects
716
     */
717
    public buildNamespaceLookup(options: { okToCache?: boolean } = { okToCache: true }) {
3,270✔
718
        let namespaceLookup = new Map<string, NamespaceContainer>();
3,270✔
719
        options.okToCache = true;
3,270✔
720
        this.enumerateBrsFiles((file) => {
3,270✔
721
            options.okToCache = options.okToCache && file.isValidated;
3,665✔
722
            const fileNamespaceLookup = file.getNamespaceLookupObject();
3,665✔
723

724
            for (const [lowerNamespaceName, nsContainer] of fileNamespaceLookup) {
3,665✔
725
                if (!namespaceLookup.has(lowerNamespaceName)) {
1,450✔
726
                    const clonedNsContainer = {
979✔
727
                        ...nsContainer,
728
                        namespaceStatements: [...nsContainer.namespaceStatements],
729
                        symbolTable: new SymbolTable(`Namespace Aggregate: '${nsContainer.fullName}'`)
730
                    };
731

732
                    clonedNsContainer.symbolTable.mergeSymbolTable(nsContainer.symbolTable);
979✔
733
                    namespaceLookup.set(lowerNamespaceName, clonedNsContainer);
979✔
734
                } else {
735
                    const existingContainer = namespaceLookup.get(lowerNamespaceName);
471✔
736
                    existingContainer.classStatements = new Map([...existingContainer.classStatements, ...nsContainer.classStatements]);
471✔
737
                    existingContainer.constStatements = new Map([...existingContainer.constStatements, ...nsContainer.constStatements]);
471✔
738
                    existingContainer.enumStatements = new Map([...existingContainer.enumStatements, ...nsContainer.enumStatements]);
471✔
739
                    existingContainer.functionStatements = new Map([...existingContainer.functionStatements, ...nsContainer.functionStatements]);
471✔
740
                    existingContainer.namespaces = new Map([...existingContainer.namespaces, ...nsContainer.namespaces]);
471✔
741
                    existingContainer.namespaceStatements.push(...nsContainer.namespaceStatements);
471✔
742
                    existingContainer.symbolTable.mergeSymbolTable(nsContainer.symbolTable);
471✔
743
                }
744
            }
745
        });
746
        return namespaceLookup;
3,270✔
747
    }
748

749
    public getAllNamespaceStatements() {
UNCOV
750
        let result = [] as NamespaceStatement[];
×
UNCOV
751
        this.enumerateBrsFiles((file) => {
×
NEW
752
            result.push(...file['_cachedLookups'].namespaceStatements);
×
753
        });
UNCOV
754
        return result;
×
755
    }
756

757
    protected logDebug(...args: any[]) {
758
        this.program.logger.debug(this._debugLogComponentName, ...args);
10,083✔
759
    }
760
    private _debugLogComponentName: string;
761

762

763
    public validationMetrics = {
3,341✔
764
        linkTime: 0,
765
        validationTime: 0
766
    };
767

768
    public validate(validationOptions: ScopeValidationOptions = { force: false }) {
1,596✔
769
        this.validationMetrics = {
4,524✔
770
            linkTime: 0,
771
            validationTime: 0
772
        };
773

774
        //if this scope is already validated, no need to revalidate
775
        if (this.isValidated === true && !validationOptions.force) {
4,524✔
776
            this.logDebug('validate(): already validated');
1,322✔
777
            return false;
1,322✔
778
        }
779

780
        if (!validationOptions.initialValidation && validationOptions.filesToBeValidatedInScopeContext?.size === 0) {
3,202✔
781
            // There was no need to validate this scope.
782
            (this as any).isValidated = true;
10✔
783
            return false;
10✔
784
        }
785

786
        this.useFileCachesForFileLinkLookups = true;//!validationOptions.initialValidation;
3,192✔
787

788
        this.program.logger.time(LogLevel.debug, [this._debugLogComponentName, 'validate()'], () => {
3,192✔
789

790
            let parentScope = this.getParentScope();
3,192✔
791

792
            //validate our parent before we validate ourself
793
            if (parentScope && parentScope.isValidated === false) {
3,192✔
794
                this.logDebug('validate(): validating parent first');
11✔
795
                parentScope.validate(validationOptions);
11✔
796
            }
797

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

800
            let t0 = performance.now();
3,192✔
801
            this.linkSymbolTable();
3,192✔
802
            this.validationMetrics.linkTime = performance.now() - t0;
3,192✔
803
            const scopeValidateEvent = {
3,192✔
804
                program: this.program,
805
                scope: this,
806
                changedFiles: new Array<BscFile>(...(validationOptions?.changedFiles?.values() ?? [])),
28,728!
807
                changedSymbols: validationOptions?.changedSymbols
9,576!
808
            };
809
            t0 = performance.now();
3,192✔
810
            this.program.plugins.emit('beforeScopeValidate', scopeValidateEvent);
3,192✔
811
            this.program.plugins.emit('onScopeValidate', scopeValidateEvent);
3,192✔
812
            this.validationMetrics.validationTime = performance.now() - t0;
3,192✔
813
            this.program.plugins.emit('afterScopeValidate', scopeValidateEvent);
3,192✔
814
            //unlink all symbol tables from this scope (so they don't accidentally stick around)
815
            this.unlinkSymbolTable();
3,192✔
816
            (this as any).isValidated = true;
3,192✔
817
        });
818
        for (let file of this.getAllFiles()) {
3,192✔
819
            validationOptions.filesToBeValidatedInScopeContext?.delete(file);
4,042✔
820
        }
821
        return true;
3,192✔
822
    }
823

824
    /**
825
     * Mark this scope as invalid, which means its `validate()` function needs to be called again before use.
826
     */
827
    public invalidate() {
828
        (this as any).isValidated = false;
5,546✔
829
        //clear out various lookups (they'll get regenerated on demand the next time they're requested)
830
        this.cache.clear();
5,546✔
831
    }
832

833
    public get symbolTable(): SymbolTable {
834
        return this.cache.getOrAdd('symbolTable', () => {
1,662,239✔
835
            const result = new SymbolTable(`Scope: '${this.name}'`, () => this.getParentScope()?.symbolTable);
337,985✔
836
            result.addSymbol('m', undefined, new AssociativeArrayType(), SymbolTypeFlag.runtime);
3,230✔
837
            for (let file of this.getOwnFiles()) {
3,230✔
838
                if (isBrsFile(file)) {
2,456✔
839
                    result.mergeSymbolTable(file.parser?.symbolTable);
2,016!
840
                }
841
            }
842
            return result;
3,230✔
843
        });
844
    }
845

846
    /**
847
     * A list of functions that will be called whenever `unlinkSymbolTable` is called
848
     */
849
    private linkSymbolTableDisposables = [];
3,341✔
850

851
    private symbolsAddedDuringLinking: { symbolTable: SymbolTable; name: string; flags: number }[] = [];
3,341✔
852

853
    public get allNamespaceTypeTable() {
854
        return this._allNamespaceTypeTable;
32✔
855
    }
856

857
    private _allNamespaceTypeTable: SymbolTable;
858

859
    /**
860
     * Builds the current symbol table for the scope, by merging the tables for all the files in this scope.
861
     * Also links all file symbols tables to this new table
862
     * This will only rebuilt if the symbol table has not been built before
863
     *
864
     *  Tree of symbol tables:
865
     *  ```
866
     *  Global Scope Symbol Table
867
     *      -  Source Scope Symbol Table :: Aggregate Namespaces Symbol Table (Siblings)
868
     *          - File 1 Symbol Table
869
     *          - File 2 Symbol Table
870
     *      -  Component A Scope Symbol Table :: Aggregate Namespaces Symbol Table (Siblings)
871
     *          - File 1 Symbol Table
872
     *          - File 2 Symbol Table
873
     *      -  Component B Scope Symbol Table :: Aggregate Namespaces Symbol Table (Siblings)
874
     *          - File 1 Symbol Table
875
     *          - File 2 Symbol Table
876
     * ```
877
     */
878
    public linkSymbolTable() {
879
        SymbolTable.cacheVerifier.generateToken();
4,074✔
880
        this._allNamespaceTypeTable = new SymbolTable(`Scope NamespaceTypes ${this.name}`);
4,074✔
881
        for (const file of this.getAllFiles()) {
4,074✔
882
            if (isBrsFile(file)) {
5,077✔
883
                this.linkSymbolTableDisposables.push(
4,543✔
884
                    file.parser.symbolTable.pushParentProvider(() => this.symbolTable)
4,715✔
885
                );
886

887
            }
888
        }
889
        this.enumerateBrsFiles((file) => {
4,074✔
890
            const namespaceTypes = file.getNamespaceSymbolTable();
4,534✔
891

892
            this.linkSymbolTableDisposables.push(
4,534✔
893
                ...this._allNamespaceTypeTable.mergeNamespaceSymbolTables(namespaceTypes)
894
            );
895
        });
896
        for (const [_, nsContainer] of this.namespaceLookup) {
4,074✔
897
            for (let nsStmt of nsContainer.namespaceStatements) {
1,245✔
898
                this.linkSymbolTableDisposables.push(
1,129✔
899
                    nsStmt?.getSymbolTable().addSibling(nsContainer.symbolTable)
3,387!
900
                );
901
            }
902
        }
903
        this.linkSymbolTableDisposables.push(
4,074✔
904
            this.symbolTable.addSibling(this._allNamespaceTypeTable)
905
        );
906
    }
907

908
    public unlinkSymbolTable() {
909
        for (const symbolToRemove of this.symbolsAddedDuringLinking) {
4,047✔
NEW
910
            this.symbolTable.removeSymbol(symbolToRemove.name);
×
911
        }
912
        this.symbolsAddedDuringLinking = [];
4,047✔
913
        for (const dispose of this.linkSymbolTableDisposables) {
4,047✔
914
            dispose();
10,661✔
915
        }
916
        this.linkSymbolTableDisposables = [];
4,047✔
917
    }
918

919
    /**
920
     * Get the list of all script imports for this scope
921
     */
922
    public getOwnScriptImports() {
923
        let result = [] as FileReference[];
1,595✔
924
        this.enumerateOwnFiles((file) => {
1,595✔
925
            if (isBrsFile(file)) {
2,393✔
926
                result.push(...file.ownScriptImports);
1,964✔
927
            } else if (isXmlFile(file)) {
429!
928
                result.push(...file.scriptTagImports);
429✔
929
            }
930
        });
931
        return result;
1,595✔
932
    }
933

934

935
    /**
936
     * Find the file with the specified relative path
937
     */
938
    public getFileByRelativePath(relativePath: string) {
939
        if (!relativePath) {
496!
940
            return;
×
941
        }
942
        let files = this.getAllFiles();
496✔
943
        for (let file of files) {
496✔
944
            if (file.destPath.toLowerCase() === relativePath.toLowerCase()) {
858✔
945
                return file;
479✔
946
            }
947
        }
948
    }
949

950
    /**
951
     * Determine if this file is included in this scope (excluding parent scopes)
952
     */
953
    public hasFile(file: BscFile) {
954
        let files = this.getOwnFiles();
19,455✔
955
        let hasFile = files.includes(file);
19,455✔
956
        return hasFile;
19,455✔
957
    }
958

959
    /**
960
     * @param className - The name of the class (including namespace if possible)
961
     * @param callsiteNamespace - the name of the namespace where the call site resides (this is NOT the known namespace of the class).
962
     *                            This is used to help resolve non-namespaced class names that reside in the same namespac as the call site.
963
     */
964
    public getClassHierarchy(className: string, callsiteNamespace?: string) {
965
        let items = [] as FileLink<ClassStatement>[];
16✔
966
        let link = this.getClassFileLink(className, callsiteNamespace);
16✔
967
        while (link) {
16✔
968
            items.push(link);
17✔
969
            link = this.getClassFileLink(link.item.parentClassName?.getName()?.toLowerCase(), callsiteNamespace);
17✔
970
        }
971
        return items;
16✔
972
    }
973
}
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