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

rokucommunity / brighterscript / #15674

29 Apr 2026 01:30PM UTC coverage: 89.093% (+0.08%) from 89.009%
#15674

push

web-flow
Merge dfbc28330 into f4cffdc58

8354 of 9876 branches covered (84.59%)

Branch coverage included in aggregate %.

176 of 180 new or added lines in 4 files covered. (97.78%)

10605 of 11404 relevant lines covered (92.99%)

2048.31 hits per line

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

91.89
/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) {
676✔
28
        super();
676✔
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;
676✔
43

44
    private ensureNameIndex() {
45
        if (this.nameSet) {
2,288✔
46
            return;
1,614✔
47
        }
48
        const nameSet = new Set<string>();
674✔
49
        const childrenByParent = new Map<string, string[]>();
674✔
50
        for (const file of this.scope.getAllFiles()) {
674✔
51
            if (isBrsFile(file)) {
854✔
52
                for (const nameLower of file.getNamespaceContributions().keys()) {
784✔
53
                    nameSet.add(nameLower);
422✔
54
                }
55
            }
56
        }
57
        for (const name of nameSet) {
674✔
58
            const lastDot = name.lastIndexOf('.');
379✔
59
            if (lastDot > 0) {
379✔
60
                const parentLower = name.substring(0, lastDot);
133✔
61
                let arr = childrenByParent.get(parentLower);
133✔
62
                if (!arr) {
133✔
63
                    arr = [];
123✔
64
                    childrenByParent.set(parentLower, arr);
123✔
65
                }
66
                arr.push(name);
133✔
67
            }
68
        }
69
        this.nameSet = nameSet;
674✔
70
        this.childrenByParent = childrenByParent;
674✔
71
    }
72

73
    private getInScopeBrsFiles(): Set<BrsFile> {
74
        if (!this.inScopeBrsFiles) {
332✔
75
            const set = new Set<BrsFile>();
224✔
76
            for (const file of this.scope.getAllFiles()) {
224✔
77
                if (isBrsFile(file)) {
314✔
78
                    set.add(file);
291✔
79
                }
80
            }
81
            this.inScopeBrsFiles = set;
224✔
82
        }
83
        return this.inScopeBrsFiles;
332✔
84
    }
85

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

98
    public override get(key: string): NamespaceContainer | undefined {
99
        const lower = key?.toLowerCase();
3,455!
100
        if (!lower) {
3,455!
NEW
101
            return undefined;
×
102
        }
103
        if (super.has(lower)) {
3,455✔
104
            return super.get(lower);
1,537✔
105
        }
106
        this.ensureNameIndex();
1,918✔
107
        if (!this.nameSet!.has(lower)) {
1,918✔
108
            //memoize the negative result so repeated misses are cheap
109
            super.set(lower, undefined as any);
1,586✔
110
            return undefined;
1,586✔
111
        }
112
        const container = this.buildContainer(lower);
332✔
113
        super.set(lower, container as any);
332✔
114
        return container;
332✔
115
    }
116

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

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

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

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

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

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

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

166
    private buildContainer(nameLower: string): NamespaceContainer | undefined {
167
        const candidateFiles = this.scope.program.getNamespaceContributors(nameLower);
332✔
168
        if (!candidateFiles || candidateFiles.size === 0) {
332!
NEW
169
            return undefined;
×
170
        }
171
        const inScopeFiles = this.getInScopeBrsFiles();
332✔
172
        const inScopeContributions: NamespaceFileContribution[] = [];
332✔
173
        for (const file of candidateFiles) {
332✔
174
            if (inScopeFiles.has(file)) {
374✔
175
                const contribution = file.getNamespaceContributions().get(nameLower);
368✔
176
                if (contribution) {
368!
177
                    inScopeContributions.push(contribution);
368✔
178
                }
179
            }
180
        }
181
        if (inScopeContributions.length === 0) {
332!
NEW
182
            return undefined;
×
183
        }
184
        if (inScopeContributions.length === 1) {
332✔
185
            return this.wrapSingleContribution(inScopeContributions[0], nameLower);
299✔
186
        }
187
        return this.aggregateContributions(inScopeContributions, nameLower);
33✔
188
    }
189

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

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

242
    /**
243
     * Build the scope-filtered children map for a parent namespace. Walks the
244
     * pre-computed `childrenByParent` index and recursively materializes each
245
     * in-scope child container.
246
     */
247
    private buildScopedChildren(parentNameLower: string): Map<string, NamespaceContainer> {
248
        const children = new Map<string, NamespaceContainer>();
332✔
249
        this.ensureNameIndex();
332✔
250
        const childNames = this.childrenByParent!.get(parentNameLower);
332✔
251
        if (!childNames) {
332✔
252
            return children;
250✔
253
        }
254
        for (const childName of childNames) {
82✔
255
            const child = this.get(childName);
90✔
256
            if (child) {
90!
257
                children.set(child.lastPartName.toLowerCase(), child);
90✔
258
            }
259
        }
260
        return children;
82✔
261
    }
262
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc