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

rokucommunity / brighterscript / #15903

11 May 2026 06:41PM UTC coverage: 86.896% (-2.2%) from 89.094%
#15903

push

web-flow
Merge 70dfd6181 into ce68f5cb7

15597 of 18958 branches covered (82.27%)

Branch coverage included in aggregate %.

9 of 9 new or added lines in 3 files covered. (100.0%)

955 existing lines in 53 files now uncovered.

16351 of 17808 relevant lines covered (91.82%)

27326.16 hits per line

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

90.54
/src/ScopeNamespaceLookup.ts
1
import type { BrsFile } from './files/BrsFile';
2
import type { Scope, NamespaceContainer, NamespaceFileContribution } from './Scope';
3
import { isBrsFile } from './astUtils/reflection';
1✔
4

5
/**
6
 * The per-scope namespace lookup. Implements `Map<string, NamespaceContainer>` so existing
7
 * plugin and internal consumers (which iterate, `.get`, `.has`, `.size`, etc.) keep working.
8
 *
9
 * Internally lazy: `get(name)` builds the container for that name only, by intersecting
10
 * the program-level `(name -> contributing files)` map with the scope's file set.
11
 *
12
 * Sharing primitives:
13
 *  - When exactly one in-scope file contributes to a namespace (the dominant case under
14
 *    a typical build), the container's heavy fields point directly at the file's pre-built
15
 *    `NamespaceFileContribution` data — including its symbolTable. Two scopes pulling in
16
 *    the same file get containers wrapping the same inner data.
17
 *  - When multiple in-scope files contribute, the container is built per-scope by merging
18
 *    contributions. This path is rare in practice but must be correct: it covers cases
19
 *    where a namespace is split across files that are all in the same scope (e.g. a shared
20
 *    declaration file plus theme-specific extensions).
21
 *
22
 * Iteration (`keys` / `values` / `entries` / `forEach`) populates every container, since
23
 * "give me everything" callers don't benefit from laziness. This matches the cost of the
24
 * pre-Phase-4 `buildNamespaceLookup` and only fires from LSP completion paths.
25
 */
26
export class ScopeNamespaceLookup extends Map<string, NamespaceContainer> {
1✔
27
    constructor(private readonly scope: Scope) {
1,070✔
28
        super();
1,070✔
29
    }
30

31
    /**
32
     * Lower-cased name parts contributed by any file in scope. Built once per lookup
33
     * instance, used as the "does this namespace exist in scope" oracle for `has` and
34
     * for driving iteration.
35
     */
36
    private nameSet: Set<string> | undefined;
37
    /** Map from lower-cased parent name to lower-cased immediate child names. */
38
    private childrenByParent: Map<string, string[]> | undefined;
39
    /** Cached `Set<BrsFile>` for `scope.getAllFiles()`, used when filtering contributors. */
40
    private inScopeBrsFiles: Set<BrsFile> | undefined;
41
    /** Set to true after `populateAll` runs so iteration doesn't keep re-checking. */
42
    private isFullyBuilt = false;
1,070✔
43

44
    private ensureNameIndex() {
45
        if (this.nameSet) {
2,569✔
46
            return;
1,501✔
47
        }
48
        const nameSet = new Set<string>();
1,068✔
49
        const childrenByParent = new Map<string, string[]>();
1,068✔
50
        for (const file of this.scope.getAllFiles()) {
1,068✔
51
            if (isBrsFile(file)) {
2,070✔
52
                // eslint-disable-next-line @typescript-eslint/dot-notation
53
                for (const nameLower of file['getNamespaceContributions']().keys()) {
1,657✔
54
                    nameSet.add(nameLower);
2,640✔
55
                }
56
            }
57
        }
58
        for (const name of nameSet) {
1,068✔
59
            const lastDot = name.lastIndexOf('.');
1,863✔
60
            if (lastDot > 0) {
1,863✔
61
                const parentLower = name.substring(0, lastDot);
755✔
62
                let arr = childrenByParent.get(parentLower);
755✔
63
                if (!arr) {
755✔
64
                    arr = [];
727✔
65
                    childrenByParent.set(parentLower, arr);
727✔
66
                }
67
                arr.push(name);
755✔
68
            }
69
        }
70
        this.nameSet = nameSet;
1,068✔
71
        this.childrenByParent = childrenByParent;
1,068✔
72
    }
73

74
    private getInScopeBrsFiles(): Set<BrsFile> {
75
        if (!this.inScopeBrsFiles) {
1,269✔
76
            const set = new Set<BrsFile>();
1,058✔
77
            for (const file of this.scope.getAllFiles()) {
1,058✔
78
                if (isBrsFile(file)) {
2,059✔
79
                    set.add(file);
1,646✔
80
                }
81
            }
82
            this.inScopeBrsFiles = set;
1,058✔
83
        }
84
        return this.inScopeBrsFiles;
1,269✔
85
    }
86

87
    public override has(key: string): boolean {
88
        const lower = key?.toLowerCase();
4!
89
        if (!lower) {
4!
90
            return false;
×
91
        }
92
        if (super.has(lower)) {
4!
UNCOV
93
            return super.get(lower) !== undefined;
×
94
        }
95
        this.ensureNameIndex();
4✔
96
        return this.nameSet!.has(lower);
4✔
97
    }
98

99
    public override get(key: string): NamespaceContainer | undefined {
100
        const lower = key?.toLowerCase();
1,996!
101
        if (!lower) {
1,996✔
102
            return undefined;
6✔
103
        }
104
        if (super.has(lower)) {
1,990✔
105
            return super.get(lower);
699✔
106
        }
107
        this.ensureNameIndex();
1,291✔
108
        if (!this.nameSet!.has(lower)) {
1,291✔
109
            //memoize the negative result so repeated misses are cheap
110
            super.set(lower, undefined as any);
22✔
111
            return undefined;
22✔
112
        }
113
        const container = this.buildContainer(lower);
1,269✔
114
        super.set(lower, container as any);
1,269✔
115
        return container;
1,269✔
116
    }
117

118
    public override get size(): number {
119
        this.ensureNameIndex();
1✔
120
        return this.nameSet!.size;
1✔
121
    }
122

123
    public override keys(): IterableIterator<string> {
124
        this.populateAll();
1✔
125
        return super.keys();
1✔
126
    }
127

128
    public override values(): IterableIterator<NamespaceContainer> {
129
        this.populateAll();
1✔
130
        return super.values();
1✔
131
    }
132

133
    public override entries(): IterableIterator<[string, NamespaceContainer]> {
134
        this.populateAll();
2✔
135
        return super.entries();
2✔
136
    }
137

138
    public override [Symbol.iterator](): IterableIterator<[string, NamespaceContainer]> {
139
        return this.entries();
1✔
140
    }
141

142
    public override forEach(
143
        callback: (value: NamespaceContainer, key: string, map: Map<string, NamespaceContainer>) => void,
144
        thisArg?: any
145
    ): void {
146
        this.populateAll();
1✔
147
        super.forEach(callback, thisArg);
1✔
148
    }
149

150
    private populateAll() {
151
        if (this.isFullyBuilt) {
5✔
152
            return;
1✔
153
        }
154
        this.ensureNameIndex();
4✔
155
        for (const name of this.nameSet!) {
4✔
156
            this.get(name);
12✔
157
        }
158
        //drop entries memoized as undefined so iteration only visits real containers
159
        for (const [key, value] of [...super.entries()]) {
4✔
160
            if (value === undefined) {
12!
UNCOV
161
                super.delete(key);
×
162
            }
163
        }
164
        this.isFullyBuilt = true;
4✔
165
    }
166

167
    private buildContainer(nameLower: string): NamespaceContainer | undefined {
168
        // eslint-disable-next-line @typescript-eslint/dot-notation
169
        const candidateFiles = this.scope.program['getNamespaceContributors'](nameLower);
1,269✔
170
        if (!candidateFiles || candidateFiles.size === 0) {
1,269!
171
            return undefined;
×
172
        }
173
        const inScopeFiles = this.getInScopeBrsFiles();
1,269✔
174
        const inScopeContributions: NamespaceFileContribution[] = [];
1,269✔
175
        for (const file of candidateFiles) {
1,269✔
176
            if (inScopeFiles.has(file)) {
31,629✔
177
                // eslint-disable-next-line @typescript-eslint/dot-notation
178
                const contribution = file['getNamespaceContributions']().get(nameLower);
1,688✔
179
                if (contribution) {
1,688!
180
                    inScopeContributions.push(contribution);
1,688✔
181
                }
182
            }
183
        }
184
        if (inScopeContributions.length === 0) {
1,269!
185
            return undefined;
×
186
        }
187
        if (inScopeContributions.length === 1) {
1,269✔
188
            return this.wrapSingleContribution(inScopeContributions[0], nameLower);
858✔
189
        }
190
        return this.aggregateContributions(inScopeContributions, nameLower);
411✔
191
    }
192

193
    /**
194
     * Fast path: single in-scope contributor. The wrapper is per-scope (so its
195
     * `namespaces` field can hold scope-specific children), but every other field
196
     * points directly at the contribution's pre-built data.
197
     */
198
    private wrapSingleContribution(contribution: NamespaceFileContribution, nameLower: string): NamespaceContainer {
199
        //field order matches the NamespaceContainer interface declaration so the
200
        //fast-path and slow-path containers share a single V8 hidden class.
201
        return {
858✔
202
            file: contribution.file,
203
            fullName: contribution.fullName,
204
            nameRange: contribution.nameRange,
205
            lastPartName: contribution.lastPartName,
206
            namespaces: this.buildScopedChildren(nameLower),
207
            namespaceStatements: contribution.namespaceStatements,
208
            statements: contribution.statements,
209
            classStatements: contribution.classStatements,
210
            functionStatements: contribution.functionStatements,
211
            enumStatements: contribution.enumStatements,
212
            constStatements: contribution.constStatements,
213
            symbolTable: contribution.symbolTable
214
        };
215
    }
216

217
    /**
218
     * Slow path: multiple in-scope contributors. The merged statement collections and
219
     * symbolTable live at the program level (`Program.getAggregateNamespaceContainer`),
220
     * keyed by `(nameLower, sorted-contributor-pkgPaths)`. Two scopes with the same
221
     * in-scope file set for this namespace share the same aggregate object, just like
222
     * the fast path shares the per-file contribution.
223
     *
224
     * The wrapper container itself is per-scope so its `namespaces` (children) field can
225
     * reflect the querying scope's file set.
226
     */
227
    private aggregateContributions(contributions: NamespaceFileContribution[], nameLower: string): NamespaceContainer {
228
        // eslint-disable-next-line @typescript-eslint/dot-notation
229
        const aggregate = this.scope.program['getAggregateNamespaceContainer'](nameLower, contributions);
411✔
230
        //field order matches the NamespaceContainer interface declaration so fast-path
231
        //and slow-path containers share a single V8 hidden class
232
        return {
411✔
233
            file: aggregate.file,
234
            fullName: aggregate.fullName,
235
            nameRange: aggregate.nameRange,
236
            lastPartName: aggregate.lastPartName,
237
            namespaces: this.buildScopedChildren(nameLower),
238
            namespaceStatements: aggregate.namespaceStatements,
239
            statements: aggregate.statements,
240
            classStatements: aggregate.classStatements,
241
            functionStatements: aggregate.functionStatements,
242
            enumStatements: aggregate.enumStatements,
243
            constStatements: aggregate.constStatements,
244
            symbolTable: aggregate.symbolTable
245
        };
246
    }
247

248
    /**
249
     * Build the scope-filtered children map for a parent namespace. Walks the
250
     * pre-computed `childrenByParent` index and recursively materializes each
251
     * in-scope child container.
252
     */
253
    private buildScopedChildren(parentNameLower: string): Map<string, NamespaceContainer> {
254
        const children = new Map<string, NamespaceContainer>();
1,269✔
255
        this.ensureNameIndex();
1,269✔
256
        const childNames = this.childrenByParent!.get(parentNameLower);
1,269✔
257
        if (!childNames) {
1,269✔
258
            return children;
1,130✔
259
        }
260
        for (const childName of childNames) {
139✔
261
            const child = this.get(childName);
158✔
262
            if (child) {
158!
263
                children.set(child.lastPartName.toLowerCase(), child);
158✔
264
            }
265
        }
266
        return children;
139✔
267
    }
268
}
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