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

rokucommunity / vscode-brightscript-language / 30100866380

24 Jul 2026 02:24PM UTC coverage: 60.103% (+0.2%) from 59.908%
30100866380

Pull #802

github

web-flow
Merge b867dd73f into e465804e3
Pull Request #802: Route extension logs through the BrightScript Extension output channel

2593 of 4745 branches covered (54.65%)

Branch coverage included in aggregate %.

64 of 102 new or added lines in 20 files covered. (62.75%)

4073 of 6346 relevant lines covered (64.18%)

43.71 hits per line

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

53.65
/src/DeclarationProvider.ts
1
import * as fs from 'fs-extra';
1✔
2
import * as iconv from 'iconv-lite';
1✔
3
import * as vscode from 'vscode';
1✔
4
import * as path from 'path';
1✔
5

6
import type {
7
    Event,
8
    Uri
9
} from 'vscode';
10
import {
1✔
11
    Disposable,
12
    EventEmitter, Location,
13
    Position,
14
    Range, SymbolInformation,
15
    SymbolKind
16
} from 'vscode';
17

18
import { BrightScriptDeclaration } from './BrightScriptDeclaration';
1✔
19
import { util } from './util';
1✔
20
import { createLogger } from './logging';
1✔
21

22
const logger = createLogger('DeclarationProvider');
1✔
23

24
///////////////////////////////////////////////////////////////////////////////////////////////////////////
25
// CREDIT WHERE CREDIT IS DUE
26
// georgejecook: I lifted most of the declaration and symbol work from sasami's era basic implementation
27
// at https://github.com/sasami/vscode-erabasic and hacked it in with some basic changes
28
///////////////////////////////////////////////////////////////////////////////////////////////////////////
29

30
export function* iterlines(input: string): IterableIterator<[number, string]> {
1✔
31
    const lines = input.split(/\r?\n/);
4✔
32
    for (let i = 0; i < lines.length; i++) {
4✔
33
        const text = lines[i];
16✔
34
        if (/^\s*(?:$|;(?![!#];))/.test(text)) {
16!
35
            continue;
×
36
        }
37
        yield [i, text];
16✔
38
    }
39
}
40

41
export class WorkspaceEncoding {
1✔
42

43
    constructor() {
44
        this.reset();
87✔
45
    }
46

47
    private encoding: string[][];
48

49
    public find(path: string): string {
50
        return this.encoding.find((v) => path.startsWith(v[0]))?.[1] ?? 'utf8';
2!
51
    }
52

53
    public reset() {
54
        this.encoding = [];
87✔
55
        for (const folder of vscode.workspace.workspaceFolders ?? []) {
87!
56
            this.encoding.push([folder.uri.fsPath, this.getConfiguration(folder.uri)]);
×
57
        }
58
    }
59

60
    private getConfiguration(uri: Uri): string {
61
        const encoding: string = util.getConfiguration('files', uri).get('encoding', 'utf8');
×
62
        if (encoding === 'utf8bom') {
×
63
            return 'utf8'; // iconv-lite removes bom by default when decoding, so this is fine
×
64
        }
65
        return encoding;
×
66
    }
67
}
68

69
export class DeclarationChangeEvent {
1✔
70
    constructor(public uri: Uri, public decls: BrightScriptDeclaration[]) {
1✔
71
    }
72
}
73

74
export class DeclarationDeleteEvent {
1✔
75
    constructor(public uri: Uri) {
1✔
76
    }
77
}
78

79
export class DeclarationProvider implements Disposable {
1✔
80
    constructor() {
81
        const subscriptions: Disposable[] = [];
82✔
82

83
        const watcher = vscode.workspace.createFileSystemWatcher('**/*.{brs,bs}');
82✔
84
        watcher.onDidCreate(this.onDidChangeFile, this);
82✔
85
        watcher.onDidChange(this.onDidChangeFile, this);
82✔
86
        watcher.onDidDelete(this.onDidDeleteFile, this);
82✔
87
        subscriptions.push(watcher);
82✔
88

89
        vscode.workspace.onDidChangeConfiguration(this.onDidChangeWorkspace, this, subscriptions);
82✔
90
        vscode.workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspace, this, subscriptions);
82✔
91

92
        this.disposable = Disposable.from(...subscriptions);
82✔
93
        void this.flush();
82✔
94
    }
95
    public cache = new Map<string, BrightScriptDeclaration[]>();
82✔
96
    private fullscan = true;
82✔
97

98
    private dirty = new Map<string, Uri>();
82✔
99
    private fileNamespaces = new Map<Uri, Set<string>>();
82✔
100
    private fileInterfaces = new Map<Uri, Set<string>>();
82✔
101
    private fileEnums = new Map<Uri, Set<string>>();
82✔
102
    private fileClasses = new Map<Uri, Set<string>>();
82✔
103
    private allNamespaces = new Map<string, BrightScriptDeclaration>();
82✔
104
    private allClasses = new Map<string, BrightScriptDeclaration>();
82✔
105
    private allEnums = new Map<string, BrightScriptDeclaration>();
82✔
106
    private allInterfaces = new Map<string, BrightScriptDeclaration>();
82✔
107

108
    private syncing: Promise<void>;
109
    private encoding: WorkspaceEncoding = new WorkspaceEncoding();
82✔
110

111
    private disposable: Disposable;
112

113
    private onDidChangeEmitter = new EventEmitter<DeclarationChangeEvent>();
82✔
114
    private onDidDeleteEmitter = new EventEmitter<DeclarationDeleteEvent>();
82✔
115
    private onDidResetEmitter = new EventEmitter<void>();
82✔
116

117
    get onDidChange(): Event<DeclarationChangeEvent> {
118
        return this.onDidChangeEmitter.event;
49✔
119
    }
120

121
    get onDidDelete(): Event<DeclarationDeleteEvent> {
122
        return this.onDidDeleteEmitter.event;
49✔
123
    }
124

125
    get onDidReset(): Event<void> {
126
        return this.onDidResetEmitter.event;
48✔
127
    }
128

129
    public async sync(): Promise<void> {
130
        if (this.syncing === undefined) {
×
131
            this.syncing = this.flush().then(() => {
×
132
                this.syncing = undefined;
×
133
            });
134
        }
135
        return this.syncing;
×
136
    }
137

138
    public dispose() {
139
        this.disposable.dispose();
×
140
    }
141

142
    private onDidChangeFile(uri: Uri) {
143
        this.dirty.set(uri.fsPath, uri);
×
144
    }
145

146
    private onDidDeleteFile(uri: Uri) {
147
        this.dirty.delete(uri.fsPath);
×
148
        this.onDidDeleteEmitter.fire(new DeclarationDeleteEvent(uri));
×
149
    }
150

151
    private onDidChangeWorkspace() {
152
        this.fullscan = true;
×
153
        this.dirty.clear();
×
154
        this.encoding.reset();
×
155
        this.onDidResetEmitter.fire();
×
156
    }
157

158
    private async flush(): Promise<void> {
159
        const excludes = getExcludeGlob();
84✔
160

161
        if (this.fullscan) {
84✔
162
            this.fullscan = false;
82✔
163

164
            for (const uri of await vscode.workspace.findFiles('**/*.{brs,bs}', excludes)) {
82✔
165
                this.dirty.set(uri.fsPath, uri);
×
166
            }
167
        }
168
        if (this.dirty.size === 0) {
84✔
169
            return;
82✔
170
        }
171
        for (const [path, uri] of Array.from(this.dirty)) {
2✔
172
            const input = await new Promise<string>((resolve, reject) => {
2✔
173
                fs.readFile(path, (err, data) => {
2✔
174
                    if (err) {
2✔
175
                        if (typeof err === 'object' && err.code === 'ENOENT') {
1!
176
                            resolve(null);
1✔
177
                        } else {
178
                            reject(err);
×
179
                        }
180
                    } else {
181
                        resolve(iconv.decode(data, this.encoding.find(path)));
1✔
182
                    }
183
                });
184
            });
185
            if (input === null) {
2✔
186
                this.dirty.delete(path);
1✔
187
                this.onDidDeleteEmitter.fire(new DeclarationDeleteEvent(uri));
1✔
188
                continue;
1✔
189
            }
190
            if (this.dirty.delete(path)) {
1!
191
                this.onDidChangeEmitter.fire(new DeclarationChangeEvent(uri, this.readDeclarations(uri, input)));
1✔
192
            }
193
        }
194
    }
195

196
    public readDeclarations(uri: Uri, input: string): BrightScriptDeclaration[] {
197
        const uriPath = util.normalizeFileScheme(uri.toString());
4✔
198
        const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri);
4✔
199
        if (workspaceFolder) {
4✔
200
            const outDir = util.normalizeFileScheme(path.join(workspaceFolder.uri.toString(), 'out'));
3✔
201

202
            // Prevents results in the out directory from being returned
203
            if (uriPath.startsWith(outDir)) {
3!
204
                return [];
×
205
            }
206
        }
207

208
        const container = BrightScriptDeclaration.fromUri(uri);
4✔
209
        const symbols: BrightScriptDeclaration[] = [];
4✔
210
        let currentFunction: BrightScriptDeclaration;
211
        let funcEndLine: number;
212
        let funcEndChar: number;
213
        let mDefs = {};
4✔
214

215
        let oldNamespaces = this.fileNamespaces.get(uri);
4✔
216
        if (oldNamespaces) {
4!
217
            for (let key of oldNamespaces.keys()) {
×
218
                let ns = this.allNamespaces.get(key);
×
219
                if (ns && ns.uri === uri) {
×
220
                    this.allNamespaces.delete(key);
×
221
                }
222
            }
223
        }
224
        this.fileNamespaces.delete(uri);
4✔
225

226
        let oldEnums = this.fileEnums.get(uri);
4✔
227
        if (oldEnums) {
4!
228
            for (let key of oldEnums.keys()) {
×
229
                let ns = this.allEnums.get(key);
×
230
                if (ns && ns.uri === uri) {
×
231
                    this.allEnums.delete(key);
×
232
                }
233
            }
234
        }
235
        this.fileEnums.delete(uri);
4✔
236

237
        let oldInterfaces = this.fileInterfaces.get(uri);
4✔
238
        if (oldInterfaces) {
4!
239
            for (let key of oldInterfaces.keys()) {
×
240
                let ns = this.allInterfaces.get(key);
×
241
                if (ns && ns.uri === uri) {
×
242
                    this.allInterfaces.delete(key);
×
243
                }
244
            }
245
        }
246
        this.fileInterfaces.delete(uri);
4✔
247

248
        let oldClasses = this.fileClasses.get(uri);
4✔
249
        if (oldClasses) {
4!
250

251
            for (let key of oldClasses.keys()) {
×
252
                let clazz = this.allClasses.get(key);
×
253
                if (clazz && clazz.uri === uri) {
×
254
                    this.allClasses.delete(key);
×
255
                }
256
            }
257
        }
258
        this.fileClasses.delete(uri);
4✔
259

260
        let namespaces = new Set<string>();
4✔
261
        let classes = new Set<string>();
4✔
262
        let enums = new Set<string>();
4✔
263
        let interfaces = new Set<string>();
4✔
264

265
        // Stack of container declarations (namespace/class/interface/enum) that are currently open.
266
        // The innermost open container is the parent for any symbols found inside it, so the outline
267
        // nests correctly. Each container's bodyRange is closed out when its matching `end` is reached.
268
        let containerStack: BrightScriptDeclaration[] = [];
4✔
269
        const currentContainer = () => (containerStack.length > 0 ? containerStack[containerStack.length - 1] : container);
8✔
270
        const closeContainer = (kind: SymbolKind, endLine: number, endChar: number) => {
4✔
271
            const openContainer = containerStack[containerStack.length - 1];
5✔
272
            if (openContainer && openContainer.kind === kind) {
5!
273
                openContainer.bodyRange = openContainer.bodyRange.with({ end: new Position(endLine, endChar) });
5✔
274
                containerStack.pop();
5✔
275
            }
276
        };
277

278
        for (const [line, text] of iterlines(input)) {
4✔
279
            // logger.log("" + line + ": " + text);
280
            funcEndLine = line;
16✔
281
            funcEndChar = text.length;
16✔
282

283
            //FUNCTION START
284
            let match = /^\s*(?:public|protected|private)*\s*(?:override)*\s*(?:function|sub)\s+(.*[^\(])\s*\((.*)\)/i.exec(text);
16✔
285
            // logger.log("match " + match);
286
            if (match !== null) {
16✔
287
                // function has started
288
                if (currentFunction !== undefined) {
3!
289
                    currentFunction.bodyRange = currentFunction.bodyRange.with({ end: new Position(funcEndLine, funcEndChar) });
×
290
                }
291
                currentFunction = new BrightScriptDeclaration(
3✔
292
                    match[1].trim(),
293
                    match[1].trim().toLowerCase() === 'new' ? SymbolKind.Constructor : SymbolKind.Function,
3!
294
                    currentContainer(),
295
                    match[2].split(','),
296
                    new Range(line, match[0].length - match[1].length - match[2].length - 2, line, match[0].length - 1),
297
                    new Range(line, 0, line, text.length)
298
                );
299
                symbols.push(currentFunction);
3✔
300
                continue;
3✔
301
            }
302

303
            //FUNCTION END
304
            match = /^\s*(end)\s*(function|sub)/i.exec(text);
13✔
305
            if (match !== null) {
13✔
306
                // logger.log("function END");
307
                if (currentFunction !== undefined) {
3!
308
                    currentFunction.bodyRange = currentFunction.bodyRange.with({ end: new Position(funcEndLine, funcEndChar) });
3✔
309
                    //reset so the function's range isn't stretched over whatever follows it (e.g. a nested namespace)
310
                    currentFunction = undefined;
3✔
311
                }
312
                continue;
3✔
313
            }
314

315
            // //FIELD
316
            match = /^(?!.*\()(?: |\t)*(public|private|protected)(?: |\t)*([a-z|\.|_]*).*((?: |\t)*=(?: |\t)*.*)*$/i.exec(text);
10✔
317
            if (match !== null) {
10!
318
                // logger.log("FOUND VAR " + match);
319
                const name = match[2].trim();
×
320
                if (mDefs[name] !== true) {
×
321
                    mDefs[name] = true;
×
322
                    let varSymbol = new BrightScriptDeclaration(
×
323
                        name,
324
                        SymbolKind.Field,
325
                        currentContainer(),
326
                        undefined,
327
                        new Range(line, match[0].length - match[1].length, line, match[0].length),
328
                        new Range(line, 0, line, text.length)
329
                    );
330
                    // logger.log('FOUND VAR ' + varSymbol.name);
331
                    symbols.push(varSymbol);
×
332
                }
333
                continue;
×
334
            }
335

336
            //start namespace declaration
337
            match = /^(?: |\t)*namespace(?: |\t)*([a-z|\.|_]*).*$/i.exec(text);
10✔
338
            if (match !== null) {
10✔
339
                const name = match[1].trim();
5✔
340
                if (name) {
5!
341
                    // For nested/dotted namespaces (e.g. `get.deep`), show only the final
342
                    // segment (`deep`) in the outline, matching the language server behavior.
343
                    const displayName = name.split('.').pop();
5✔
344
                    const namespaceSymbol = new BrightScriptDeclaration(
5✔
345
                        displayName,
346
                        SymbolKind.Namespace,
347
                        currentContainer(),
348
                        undefined,
349
                        new Range(line, match[0].length - match[1].length, line, match[0].length),
350
                        new Range(line, 0, line, text.length)
351
                    );
352
                    // logger.log('FOUND NAMESPACES ' + namespaceSymbol.name);
353
                    symbols.push(namespaceSymbol);
5✔
354
                    namespaces.add(name.toLowerCase());
5✔
355
                    containerStack.push(namespaceSymbol);
5✔
356
                }
357
                continue;
5✔
358
            }
359
            //end namespace declaration
360
            match = /^(?: |\t)*end namespace.*$/i.exec(text);
5✔
361
            if (match !== null) {
5!
362
                closeContainer(SymbolKind.Namespace, line, text.length);
5✔
363
                continue;
5✔
364
            }
365

366
            //start enum declaration
367
            match = /^(?: |\t)*enum(?: |\t)*([a-z|\.|_]*).*$/i.exec(text);
×
368
            if (match !== null) {
×
369
                const name = match[1].trim();
×
370
                if (name) {
×
371
                    const enumSymbol = new BrightScriptDeclaration(
×
372
                        name,
373
                        SymbolKind.Enum,
374
                        currentContainer(),
375
                        undefined,
376
                        new Range(line, match[0].length - match[1].length, line, match[0].length),
377
                        new Range(line, 0, line, text.length)
378
                    );
379
                    // logger.log('FOUND enumS ' + enumSymbol.name);
380
                    symbols.push(enumSymbol);
×
381
                    enums.add(name.toLowerCase());
×
382
                    containerStack.push(enumSymbol);
×
383
                }
384
                continue;
×
385
            }
386
            //end enum declaration
387
            match = /^(?: |\t)*end enum.*$/i.exec(text);
×
388
            if (match !== null) {
×
389
                closeContainer(SymbolKind.Enum, line, text.length);
×
390
                continue;
×
391
            }
392

393
            //start class declaration
394
            match = /(?:(class)\s+([a-z_][a-z0-9_]*))\s*(?:extends\s*([a-z_][a-z0-9_]+))*$/i.exec(text);
×
395
            if (match !== null) {
×
396
                const name = match[2].trim();
×
397
                if (name) {
×
398
                    const classSymbol = new BrightScriptDeclaration(
×
399
                        name,
400
                        SymbolKind.Class,
401
                        currentContainer(),
402
                        undefined,
403
                        new Range(line, match[0].length - match[2].length, line, match[0].length),
404
                        new Range(line, 0, line, text.length)
405
                    );
406
                    // logger.log('FOUND CLASS ' + classSymbol.name);
407
                    symbols.push(classSymbol);
×
408
                    classes.add(name.toLowerCase());
×
409
                    containerStack.push(classSymbol);
×
410
                }
411
                continue;
×
412
            }
413
            //end class declaration
414
            match = /^(?: |\t)*end class.*$/i.exec(text);
×
415
            if (match !== null) {
×
416
                closeContainer(SymbolKind.Class, line, text.length);
×
417
                continue;
×
418
            }
419

420
            //start interface declaration
421
            match = /(?:(interface)\s+([a-z_][a-z0-9_]*))\s*(?:extends\s*([a-z_][a-z0-9_]+))*$/i.exec(text);
×
422
            if (match !== null) {
×
423
                const name = match[2].trim();
×
424
                if (name) {
×
425
                    const interfaceSymbol = new BrightScriptDeclaration(
×
426
                        name,
427
                        SymbolKind.Interface,
428
                        currentContainer(),
429
                        undefined,
430
                        new Range(line, match[0].length - match[2].length, line, match[0].length),
431
                        new Range(line, 0, line, text.length)
432
                    );
433
                    // logger.log('FOUND interface ' + interfaceSymbol.name);
434
                    symbols.push(interfaceSymbol);
×
435
                    interfaces.add(name.toLowerCase());
×
436
                    containerStack.push(interfaceSymbol);
×
437
                }
438
                continue;
×
439
            }
440
            //end interface declaration
441
            match = /^(?: |\t)*end interface.*$/i.exec(text);
×
442
            if (match !== null) {
×
443
                closeContainer(SymbolKind.Interface, line, text.length);
×
444
                continue;
×
445
            }
446

447
        }
448
        this.fileNamespaces.set(uri, namespaces);
4✔
449
        this.fileClasses.set(uri, classes);
4✔
450
        this.cache.set(uri.fsPath, symbols);
4✔
451
        return symbols;
4✔
452
    }
453

454
    public declToSymbolInformation(uri: Uri, decl: BrightScriptDeclaration): SymbolInformation {
455
        return new SymbolInformation(
×
456
            decl.name,
457
            decl.kind,
458
            decl.containerName ? decl.containerName : decl.name,
×
459
            new Location(uri, decl.bodyRange)
460
        );
461
    }
462

463
    public getFunctionBeforeLine(filePath: string, lineNumber: number): BrightScriptDeclaration | null {
464
        let symbols = this.cache.get(filePath);
×
465
        if (!symbols) {
×
466
            try {
×
467

468
                let uri = vscode.Uri.file(filePath);
×
469
                let decls = this.readDeclarations(uri, fs.readFileSync(filePath, 'utf8'));
×
470
                this.cache.set(filePath, decls);
×
471
                // if there was no match, then get the declarations now
472
                symbols = this.cache.get(filePath);
×
473
            } catch (e) {
NEW
474
                logger.error(`error loading symbols for file ${filePath}: ${e.message}`);
×
475
            }
476
        }
477
        //try to load it now
478
        if (symbols) {
×
479
            const matchingMethods = symbols
×
480
                .filter((symbol) => symbol.kind === SymbolKind.Function && symbol.nameRange.start.line < lineNumber);
×
481
            return matchingMethods.length > 0 ? matchingMethods[matchingMethods.length - 1] : null;
×
482
        }
483
        return null;
×
484
    }
485

486
}
487

488
export function getExcludeGlob(): string {
1✔
489
    const exclude = [
84✔
490
        ...Object.keys(util.getConfiguration('search').get('exclude') || {}),
168✔
491
        ...Object.keys(util.getConfiguration('files').get('exclude') || {})
168✔
492
    ].join(',');
493
    return `{${exclude}}`;
84✔
494
}
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