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

rokucommunity / brighterscript / #13978

10 Mar 2025 01:00PM UTC coverage: 86.759% (-2.4%) from 89.113%
#13978

push

web-flow
Merge 96004e807 into 700183e7d

12688 of 15459 branches covered (82.08%)

Branch coverage included in aggregate %.

28 of 28 new or added lines in 1 file covered. (100.0%)

924 existing lines in 53 files now uncovered.

13614 of 14857 relevant lines covered (91.63%)

19945.24 hits per line

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

90.0
/src/SymbolTable.ts
1
import type { BscType } from './types/BscType';
2
import type { ExtraSymbolData, GetTypeOptions } from './interfaces';
3
import { CacheVerifier } from './CacheVerifier';
1✔
4
import type { ReferenceType } from './types/ReferenceType';
5
import type { UnionType } from './types/UnionType';
6
import { getUniqueType } from './types/helpers';
1✔
7
import { isAnyReferenceType, isNamespaceType, isReferenceType } from './astUtils/reflection';
1✔
8
import { SymbolTypeFlag } from './SymbolTypeFlag';
1✔
9

10
/**
11
 * Stores the types associated with variables and functions in the Brighterscript code
12
 * Can be part of a hierarchy, so lookups can reference parent scopes
13
 */
14
export class SymbolTable implements SymbolTypeGetter {
1✔
15
    constructor(
16
        public name: string,
1,410,094✔
17
        parentProvider?: SymbolTableProvider
18
    ) {
19
        this.resetTypeCache();
1,410,094✔
20
        if (parentProvider) {
1,410,094✔
21
            this.pushParentProvider(parentProvider);
388,247✔
22
        }
23
    }
24

25
    /**
26
     * The map of symbols declared directly in this SymbolTable (excludes parent SymbolTable).
27
     * Indexed by lower symbol name
28
     */
29
    private symbolMap = new Map<string, BscSymbol[]>();
1,410,094✔
30

31
    private parentProviders = [] as SymbolTableProvider[];
1,410,094✔
32

33
    private cacheToken: string;
34

35
    private typeCache: Array<Map<string, TypeCacheEntry>>;
36

37
    /**
38
     * Used to invalidate the cache for all symbol tables.
39
     *
40
     * This is not the most optimized solution as cache will be shared across all instances of SymbolTable across all programs,
41
     * but this is the easiest way to handle nested/linked symbol table cache management so we can optimize this in the future some time...
42
     */
43
    static cacheVerifier = new CacheVerifier();
1✔
44

45
    static referenceTypeFactory: (memberKey: string, fullName, flags: SymbolTypeFlag, tableProvider: SymbolTypeGetterProvider) => ReferenceType;
46
    static unionTypeFactory: (types: BscType[]) => UnionType;
47

48
    /**
49
     * Push a function that will provide a parent SymbolTable when requested
50
     */
51
    public pushParentProvider(provider: SymbolTableProvider) {
52
        this.cachedCircularReferenceCheck = null;
580,921✔
53
        this.parentProviders.push(provider);
580,921✔
54
        return () => {
580,921✔
55
            this.popParentProvider();
10,440✔
56
        };
57
    }
58

59
    /**
60
     * Pop the current parentProvider
61
     */
62
    public popParentProvider() {
63
        this.cachedCircularReferenceCheck = null;
10,440✔
64
        this.parentProviders.pop();
10,440✔
65
    }
66

67
    /**
68
     * The parent SymbolTable (if there is one)
69
     */
70
    public get parent(): SymbolTable | undefined {
71
        return this.parentProviders[this.parentProviders.length - 1]?.();
586,978✔
72
    }
73

74
    private siblings = new Set<SymbolTable>();
1,410,094✔
75

76
    /**
77
     * Add a sibling symbol table (which will be inspected first before walking upward to the parent
78
     */
79
    public addSibling(sibling: SymbolTable) {
80
        this.siblings.add(sibling);
13,166✔
81
        return () => {
13,166✔
82
            this.siblings.delete(sibling);
9,603✔
83
        };
84
    }
85

86
    /**
87
     * Remove a sibling symbol table
88
     */
89
    public removeSibling(sibling: SymbolTable) {
UNCOV
90
        this.siblings.delete(sibling);
×
91
    }
92

93

94
    public clearSymbols() {
UNCOV
95
        this.symbolMap.clear();
×
96
    }
97

98
    /**
99
     * Checks if the symbol table contains the given symbol by name
100
     * If the identifier is not in this table, it will check the parent
101
     *
102
     * @param name the name to lookup
103
     * @param bitFlags flags to match (See SymbolTypeFlags)
104
     * @returns true if this symbol is in the symbol table
105
     */
106
    hasSymbol(name: string, bitFlags: SymbolTypeFlag): boolean {
107
        let currentTable: SymbolTable = this;
358,110✔
108
        const key = name?.toLowerCase();
358,110!
109
        let result: BscSymbol[];
110
        do {
358,110✔
111
            // look in our map first
112
            if ((result = currentTable.symbolMap.get(key))) {
364,944✔
113
                // eslint-disable-next-line no-bitwise
114
                if (result.find(symbol => symbol.flags & bitFlags)) {
170,083✔
115
                    return true;
170,081✔
116
                }
117
            }
118

119
            //look through any sibling maps next
120
            for (let sibling of currentTable.siblings) {
194,863✔
121
                if ((result = sibling.symbolMap.get(key))) {
2,093✔
122
                    // eslint-disable-next-line no-bitwise
123
                    if (result.find(symbol => symbol.flags & bitFlags)) {
331!
124
                        return true;
331✔
125
                    }
126
                }
127
            }
128
            currentTable = currentTable.parent;
194,532✔
129
        } while (currentTable);
130
        return false;
187,698✔
131
    }
132

133
    /**
134
     * Gets the name/type pair for a given named variable or function name
135
     * If the identifier is not in this table, it will check the parent
136
     *
137
     * @param  name the name to lookup
138
     * @param bitFlags flags to match
139
     * @returns An array of BscSymbols - one for each time this symbol had a type implicitly defined
140
     */
141
    getSymbol(name: string, bitFlags: SymbolTypeFlag): BscSymbol[] {
142
        let currentTable: SymbolTable = this;
450,994✔
143
        const key = name?.toLowerCase();
450,994✔
144
        let result: BscSymbol[];
145
        let memberOfAncestor = false;
450,994✔
146
        const addAncestorInfo = (symbol: BscSymbol) => ({ ...symbol, data: { ...symbol.data, memberOfAncestor: memberOfAncestor } });
450,994✔
147
        do {
450,994✔
148
            // look in our map first
149
            if ((result = currentTable.symbolMap.get(key))) {
629,598✔
150
                // eslint-disable-next-line no-bitwise
151
                result = result.filter(symbol => symbol.flags & bitFlags);
246,351✔
152
                if (result.length > 0) {
246,209✔
153
                    return result.map(addAncestorInfo);
245,662✔
154
                }
155
            }
156
            //look through any sibling maps next
157
            for (let sibling of currentTable.siblings) {
383,936✔
158
                result = sibling.getSymbol(key, bitFlags);
54,439✔
159
                if (result?.length > 0) {
54,439✔
160
                    return result.map(addAncestorInfo);
2,719✔
161
                }
162
            }
163
            currentTable = currentTable.parent;
381,217✔
164
            memberOfAncestor = true;
381,217✔
165
        } while (currentTable);
166
        return result;
202,613✔
167
    }
168

169
    /**
170
     * Adds a new symbol to the table
171
     */
172
    addSymbol(name: string, data: ExtraSymbolData, type: BscType, bitFlags: SymbolTypeFlag) {
173
        if (!name) {
6,837,051✔
174
            return;
1✔
175
        }
176
        const key = name?.toLowerCase();
6,837,050!
177
        if (!this.symbolMap.has(key)) {
6,837,050✔
178
            this.symbolMap.set(key, []);
6,806,560✔
179
        }
180
        this.symbolMap.get(key)?.push({
6,837,050!
181
            name: name,
182
            data: data,
183
            type: type,
184
            flags: bitFlags
185
        });
186
    }
187

188
    /**
189
     * Removes a new symbol from the table
190
     */
191
    removeSymbol(name: string) {
192
        const key = name.toLowerCase();
2,488✔
193
        if (!this.symbolMap.has(key)) {
2,488✔
194
            this.symbolMap.set(key, []);
477✔
195
        }
196
        this.symbolMap.delete(key);
2,488✔
197
    }
198

199
    public getSymbolTypes(name: string, options: GetSymbolTypeOptions): TypeCacheEntry[] {
200
        const symbolArray = this.getSymbol(name, options.flags);
390,968✔
201
        if (!symbolArray) {
390,968✔
202
            return undefined;
145,875✔
203
        }
204
        return symbolArray?.map(symbol => ({ type: symbol.type, data: symbol.data, flags: symbol.flags }));
245,170!
205
    }
206

207
    getSymbolType(name: string, options: GetSymbolTypeOptions): BscType {
208
        const cacheEntry = options.ignoreCacheForRetrieval ? undefined : this.getCachedType(name, options);
767,596!
209
        let resolvedType = cacheEntry?.type;
767,596✔
210
        let doSetCache = !resolvedType;
767,596✔
211
        const originalIsReferenceType = isAnyReferenceType(resolvedType);
767,596✔
212
        let data = cacheEntry?.data || {} as ExtraSymbolData;
767,596✔
213
        let foundFlags: SymbolTypeFlag = cacheEntry?.flags;
767,596✔
214
        if (!resolvedType || originalIsReferenceType) {
767,596✔
215
            const symbolTypes = this.getSymbolTypes(name, options);
363,941✔
216
            data = symbolTypes?.[0]?.data;
363,941✔
217
            foundFlags = symbolTypes?.[0]?.flags;
363,941✔
218
            resolvedType = getUniqueType(symbolTypes?.map(symbol => symbol.type), SymbolTable.unionTypeFactory);
363,941✔
219
        }
220
        if (!resolvedType && options.fullName && options.tableProvider) {
767,596✔
221
            resolvedType = SymbolTable.referenceTypeFactory(name, options.fullName, options.flags, options.tableProvider);
108,501✔
222
        }
223
        const resolvedTypeIsReference = isAnyReferenceType(resolvedType);
767,596✔
224
        const newNonReferenceType = originalIsReferenceType && !isAnyReferenceType(resolvedType);
767,596✔
225
        doSetCache = doSetCache && (options.onlyCacheResolvedTypes ? !resolvedTypeIsReference : true);
767,596✔
226
        if (doSetCache || newNonReferenceType) {
767,596✔
227
            this.setCachedType(name, { type: resolvedType, data: data, flags: foundFlags }, options);
361,542✔
228
        }
229
        options.data ??= {};
767,593✔
230
        if (options.data) {
767,593!
231
            options.data.definingNode = data?.definingNode;
767,593✔
232
            options.data.description = data?.description;
767,593✔
233
            options.data.flags = foundFlags ?? options.flags;
767,593✔
234
            options.data.memberOfAncestor = data?.memberOfAncestor;
767,593✔
235
            options.data.doNotMerge = data?.doNotMerge;
767,593✔
236
            options.data.isAlias = data?.isAlias;
767,593✔
237
            options.data.isInstance = data?.isInstance;
767,593✔
238
            options.data.isFromDocComment = data?.isFromDocComment;
767,593✔
239
            options.data.isBuiltIn = data?.isBuiltIn;
767,593✔
240
            options.data.isFromCallFunc = data?.isFromCallFunc;
767,593✔
241
        }
242
        return resolvedType;
767,593✔
243
    }
244

245
    isSymbolTypeInstance(name: string) {
246
        const data: ExtraSymbolData = {};
4✔
247
        this.getSymbolType(name, { flags: SymbolTypeFlag.runtime, data: data });
4✔
248
        return data?.isInstance;
4!
249
    }
250

251
    /**
252
     * Adds all the symbols from another table to this one
253
     * It will overwrite any existing symbols in this table
254
     */
255
    mergeSymbolTable(symbolTable: SymbolTable) {
256
        for (let [, value] of symbolTable.symbolMap) {
6,609✔
257
            for (const symbol of value) {
5,692✔
258
                if (symbol.data?.doNotMerge) {
5,712✔
259
                    continue;
51✔
260
                }
261
                this.addSymbol(
5,661✔
262
                    symbol.name,
263
                    symbol.data,
264
                    symbol.type,
265
                    symbol.flags
266
                );
267
            }
268
        }
269
    }
270

271
    mergeNamespaceSymbolTables(symbolTable: SymbolTable) {
272
        const disposables = [] as Array<() => void>;
8,751✔
273
        for (let [_name, value] of symbolTable.symbolMap) {
8,751✔
274
            const symbol = value[0];
2,463✔
275
            if (symbol) {
2,463!
276
                if (symbol.data?.doNotMerge) {
2,463!
UNCOV
277
                    continue;
×
278
                }
279
                const existingRuntimeType = this.getSymbolType(symbol.name, { flags: symbol.flags });
2,463✔
280

281
                if (isNamespaceType(existingRuntimeType) && isNamespaceType(symbol.type)) {
2,463✔
282
                    disposables.push(...existingRuntimeType.memberTable.mergeNamespaceSymbolTables(symbol.type.memberTable));
908✔
283
                } else {
284
                    this.addSymbol(
1,555✔
285
                        symbol.name,
286
                        symbol.data,
287
                        symbol.type,
288
                        symbol.flags
289
                    );
290
                    disposables.push(() => this.removeSymbol(symbol.name));
1,555✔
291
                }
292
            }
293
        }
294

295
        for (let siblingTable of symbolTable.siblings) {
8,751✔
296
            disposables.push(...this.mergeNamespaceSymbolTables(siblingTable));
906✔
297
        }
298
        return disposables;
8,751✔
299
    }
300

301
    /**
302
     * Get list of symbols declared directly in this SymbolTable (excludes parent SymbolTable).
303
     */
304
    public getOwnSymbols(bitFlags: SymbolTypeFlag): BscSymbol[] {
305
        let symbols: BscSymbol[] = [].concat(...this.symbolMap.values());
5,002✔
306
        // eslint-disable-next-line no-bitwise
307
        symbols = symbols.filter(symbol => symbol.flags & bitFlags);
25,502✔
308
        return symbols;
5,002✔
309
    }
310

311

312
    private cachedCircularReferenceCheck: null | boolean = null;
1,410,094✔
313

314
    private hasCircularReferenceWithAncestor() {
315
        if (this.cachedCircularReferenceCheck === false || this.cachedCircularReferenceCheck === true) {
2,393✔
316
            return this.cachedCircularReferenceCheck;
1,185✔
317
        }
318
        let foundCircReference = false;
1,208✔
319
        let p = this.parent;
1,208✔
320
        while (!foundCircReference && p) {
1,208✔
321
            foundCircReference = p === this;
2,169✔
322
            p = p.parent;
2,169✔
323
        }
324
        this.cachedCircularReferenceCheck = foundCircReference;
1,208✔
325
        return foundCircReference;
1,208✔
326
    }
327

328
    /**
329
     * Get list of all symbols declared in this SymbolTable (includes parent SymbolTable).
330
     */
331
    public getAllSymbols(bitFlags: SymbolTypeFlag): BscSymbol[] {
332
        let symbols: BscSymbol[] = [].concat(...this.symbolMap.values());
5,469✔
333
        //look through any sibling maps next
334
        for (let sibling of this.siblings) {
5,469✔
335
            symbols = symbols.concat(sibling.getAllSymbols(bitFlags));
32✔
336
        }
337

338
        if (this.parent && !this.hasCircularReferenceWithAncestor()) {
5,469✔
339
            symbols = symbols.concat(this.parent.getAllSymbols(bitFlags));
2,383✔
340
        }
341
        // eslint-disable-next-line no-bitwise
342
        symbols = symbols.filter(symbol => symbol.flags & bitFlags);
128,371✔
343

344
        //remove duplicate symbols
345
        const symbolsMap = new Map<string, BscSymbol>();
5,469✔
346
        for (const symbol of symbols) {
5,469✔
347
            const lowerSymbolName = symbol.name.toLowerCase();
128,371✔
348
            if (!symbolsMap.has(lowerSymbolName)) {
128,371✔
349
                symbolsMap.set(lowerSymbolName, symbol);
127,786✔
350
            }
351
        }
352
        return [...symbolsMap.values()];
5,469✔
353
    }
354

355
    private resetTypeCache() {
356
        this.typeCache = [
1,416,523✔
357
            undefined,
358
            new Map<string, TypeCacheEntry>(), // SymbolTypeFlags.runtime
359
            new Map<string, TypeCacheEntry>(), // SymbolTypeFlags.typetime
360
            new Map<string, TypeCacheEntry>() // SymbolTypeFlags.runtime & SymbolTypeFlags.typetime
361
        ];
362
        this.cacheToken = SymbolTable.cacheVerifier?.getToken();
1,416,523!
363
    }
364

365
    getCachedType(name: string, options: GetTypeOptions): TypeCacheEntry {
366
        if (SymbolTable.cacheVerifier) {
767,596!
367
            if (!SymbolTable.cacheVerifier?.checkToken(this.cacheToken)) {
767,596!
368
                // we have a bad token
369
                this.resetTypeCache();
6,429✔
370
                return;
6,429✔
371
            }
372
        } else {
373
            // no cache verifier
UNCOV
374
            return;
×
375
        }
376
        return this.typeCache[options.flags]?.get(name.toLowerCase());
761,167!
377
    }
378

379
    setCachedType(name: string, cacheEntry: TypeCacheEntry, options: GetTypeOptions) {
380
        if (!cacheEntry) {
362,323!
UNCOV
381
            return;
×
382
        }
383
        if (SymbolTable.cacheVerifier) {
362,323!
384
            if (!SymbolTable.cacheVerifier?.checkToken(this.cacheToken)) {
362,323!
385
                // we have a bad token - remove all other caches
UNCOV
386
                this.resetTypeCache();
×
387
            }
388
        } else {
389
            // no cache verifier
UNCOV
390
            return;
×
391
        }
392
        let existingCachedValue = this.typeCache[options.flags]?.get(name.toLowerCase());
362,323!
393
        if (isReferenceType(cacheEntry.type) && !isReferenceType(existingCachedValue)) {
362,320✔
394
            // No need to overwrite a non-referenceType with a referenceType
395
            return;
108,525✔
396
        }
397
        return this.typeCache[options.flags]?.set(name.toLowerCase(), cacheEntry);
253,795!
398
    }
399

400
    /**
401
     * Serialize this SymbolTable to JSON (useful for debugging reasons)
402
     */
403
    private toJSON() {
UNCOV
404
        return {
×
405
            name: this.name,
UNCOV
406
            siblings: [...this.siblings].map(sibling => sibling.toJSON()),
×
407
            parent: this.parent?.toJSON(),
×
408
            symbols: [
409
                ...new Set(
410
                    [...this.symbolMap.entries()].map(([key, symbols]) => {
UNCOV
411
                        return symbols.map(x => {
×
UNCOV
412
                            return { name: x.name, type: (x.type as any)?.__identifier };
×
413
                        });
414
                    }).flat().sort()
415
                )
416
            ]
417
        };
418
    }
419
}
420

421
export interface BscSymbol {
422
    name: string;
423
    data: ExtraSymbolData;
424
    type: BscType;
425
    flags: SymbolTypeFlag;
426
}
427

428
export interface BscSymbolWithSource extends BscSymbol {
429
    comesFromAncestor: boolean; // this symbol comes from an ancestor symbol table
430
}
431

432
export interface SymbolTypeGetter {
433
    name: string;
434
    getSymbolType(name: string, options: GetSymbolTypeOptions): BscType;
435
    setCachedType(name: string, cacheEntry: TypeCacheEntry, options: GetSymbolTypeOptions);
436
    addSibling(symbolTable: SymbolTable);
437
}
438

439
/**
440
 * A function that returns a symbol table.
441
 */
442
export type SymbolTableProvider = () => SymbolTable;
443

444
/**
445
 * A function that returns a symbol types getter - smaller interface used in types
446
 */
447
export type SymbolTypeGetterProvider = () => SymbolTypeGetter;
448

449

450
export interface GetSymbolTypeOptions extends GetTypeOptions {
451
    fullName?: string;
452
    tableProvider?: SymbolTableProvider;
453
}
454

455
export interface TypeCacheEntry {
456
    type: BscType;
457
    data?: ExtraSymbolData;
458
    flags?: SymbolTypeFlag;
459
}
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