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

rokucommunity / vscode-brightscript-language / #2719

24 Aug 2022 12:51PM UTC coverage: 41.691% (+0.02%) from 41.675%
#2719

push

TwitchBronBron
2.35.0

477 of 1427 branches covered (33.43%)

Branch coverage included in aggregate %.

1126 of 2418 relevant lines covered (46.57%)

7.32 hits per line

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

19.5
/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

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

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

38
export class WorkspaceEncoding {
1✔
39

40
    constructor() {
41
        this.reset();
59✔
42
    }
43

44
    private encoding: string[][];
45

46
    public find(path: string): string {
47
        return this.encoding.find((v) => path.startsWith(v[0]))[1];
×
48
    }
49

50
    public reset() {
51
        this.encoding = [];
59✔
52
        for (const folder of vscode.workspace.workspaceFolders) {
59✔
53
            this.encoding.push([folder.uri.fsPath, this.getConfiguration(folder.uri)]);
217✔
54
        }
55
    }
56

57
    private getConfiguration(uri: Uri): string {
58
        const encoding: string = vscode.workspace.getConfiguration('files', uri).get('encoding', 'utf8');
217✔
59
        if (encoding === 'utf8bom') {
217!
60
            return 'utf8'; // iconv-lite removes bom by default when decoding, so this is fine
×
61
        }
62
        return encoding;
217✔
63
    }
64
}
65

66
export class DeclarationChangeEvent {
1✔
67
    constructor(public uri: Uri, public decls: BrightScriptDeclaration[]) {
×
68
    }
69
}
70

71
export class DeclarationDeleteEvent {
1✔
72
    constructor(public uri: Uri) {
×
73
    }
74
}
75

76
export class DeclarationProvider implements Disposable {
1✔
77
    constructor() {
78
        const subscriptions: Disposable[] = [];
55✔
79

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

86
        vscode.workspace.onDidChangeConfiguration(this.onDidChangeWorkspace, this, subscriptions);
55✔
87
        vscode.workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspace, this, subscriptions);
55✔
88

89
        this.disposable = Disposable.from(...subscriptions);
55✔
90
        void this.flush();
55✔
91
    }
92
    public cache: Map<string, BrightScriptDeclaration[]> = new Map();
55✔
93
    private fullscan = true;
55✔
94

95
    private dirty: Map<string, Uri> = new Map();
55✔
96
    private fileNamespaces = new Map<Uri, Set<string>>();
55✔
97
    private fileInterfaces = new Map<Uri, Set<string>>();
55✔
98
    private fileEnums = new Map<Uri, Set<string>>();
55✔
99
    private fileClasses = new Map<Uri, Set<string>>();
55✔
100
    private allNamespaces = new Map<string, BrightScriptDeclaration>();
55✔
101
    private allClasses = new Map<string, BrightScriptDeclaration>();
55✔
102
    private allEnums = new Map<string, BrightScriptDeclaration>();
55✔
103
    private allInterfaces = new Map<string, BrightScriptDeclaration>();
55✔
104

105
    private syncing: Promise<void>;
106
    private encoding: WorkspaceEncoding = new WorkspaceEncoding();
55✔
107

108
    private disposable: Disposable;
109

110
    private onDidChangeEmitter: EventEmitter<DeclarationChangeEvent> = new EventEmitter();
55✔
111
    private onDidDeleteEmitter: EventEmitter<DeclarationDeleteEvent> = new EventEmitter();
55✔
112
    private onDidResetEmitter: EventEmitter<void> = new EventEmitter();
55✔
113

114
    get onDidChange(): Event<DeclarationChangeEvent> {
115
        return this.onDidChangeEmitter.event;
25✔
116
    }
117

118
    get onDidDelete(): Event<DeclarationDeleteEvent> {
119
        return this.onDidDeleteEmitter.event;
25✔
120
    }
121

122
    get onDidReset(): Event<void> {
123
        return this.onDidResetEmitter.event;
25✔
124
    }
125

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

135
    public dispose() {
136
        this.disposable.dispose();
×
137
    }
138

139
    private onDidChangeFile(uri: Uri) {
140
        const excludes = getExcludeGlob();
×
141
        this.dirty.set(uri.fsPath, uri);
×
142
    }
143

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

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

156
    private async flush(): Promise<void> {
157
        const excludes = getExcludeGlob();
55✔
158

159
        if (this.fullscan) {
55!
160
            this.fullscan = false;
55✔
161

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

194
    public readDeclarations(uri: Uri, input: string): BrightScriptDeclaration[] {
195
        const uriPath = util.normalizeFileScheme(uri.toString());
×
196
        const outDir = util.normalizeFileScheme(path.join(vscode.workspace.getWorkspaceFolder(uri).uri.toString(), 'out'));
×
197

198
        // Prevents results in the out directory from being returned
199
        if (uriPath.startsWith(outDir)) {
×
200
            return;
×
201
        }
202

203
        const container = BrightScriptDeclaration.fromUri(uri);
×
204
        const symbols: BrightScriptDeclaration[] = [];
×
205
        let currentFunction: BrightScriptDeclaration;
206
        let funcEndLine: number;
207
        let funcEndChar: number;
208
        let mDefs = {};
×
209

210
        let oldNamespaces = this.fileNamespaces.get(uri);
×
211
        if (oldNamespaces) {
×
212
            for (let key of oldNamespaces.keys()) {
×
213
                let ns = this.allNamespaces.get(key);
×
214
                if (ns && ns.uri === uri) {
×
215
                    this.allNamespaces.delete(key);
×
216
                }
217
            }
218
        }
219
        this.fileNamespaces.delete(uri);
×
220

221
        let oldEnums = this.fileEnums.get(uri);
×
222
        if (oldEnums) {
×
223
            for (let key of oldEnums.keys()) {
×
224
                let ns = this.allEnums.get(key);
×
225
                if (ns && ns.uri === uri) {
×
226
                    this.allEnums.delete(key);
×
227
                }
228
            }
229
        }
230
        this.fileEnums.delete(uri);
×
231

232
        let oldInterfaces = this.fileInterfaces.get(uri);
×
233
        if (oldInterfaces) {
×
234
            for (let key of oldInterfaces.keys()) {
×
235
                let ns = this.allInterfaces.get(key);
×
236
                if (ns && ns.uri === uri) {
×
237
                    this.allInterfaces.delete(key);
×
238
                }
239
            }
240
        }
241
        this.fileInterfaces.delete(uri);
×
242

243
        let oldClasses = this.fileClasses.get(uri);
×
244
        if (oldClasses) {
×
245

246
            for (let key of oldClasses.keys()) {
×
247
                let clazz = this.allClasses.get(key);
×
248
                if (clazz && clazz.uri === uri) {
×
249
                    this.allClasses.delete(key);
×
250
                }
251
            }
252
        }
253
        this.fileClasses.delete(uri);
×
254

255
        let namespaces = new Set<string>();
×
256
        let classes = new Set<string>();
×
257
        let namespaceSymbol: BrightScriptDeclaration | null;
258
        let classSymbol: BrightScriptDeclaration | null;
259
        let enums = new Set<string>();
×
260
        let interfaces = new Set<string>();
×
261
        let interfaceSymbol: BrightScriptDeclaration | null;
262
        let enumSymbol: BrightScriptDeclaration | null;
263

264
        for (const [line, text] of iterlines(input)) {
×
265
            // console.log("" + line + ": " + text);
266
            funcEndLine = line;
×
267
            funcEndChar = text.length;
×
268

269
            //FUNCTION START
270
            let match = /^\s*(?:public|protected|private)*\s*(?:override)*\s*(?:function|sub)\s+(.*[^\(])\s*\((.*)\)/i.exec(text);
×
271
            // console.log("match " + match);
272
            if (match !== null) {
×
273
                // function has started
274
                if (currentFunction !== undefined) {
×
275
                    currentFunction.bodyRange = currentFunction.bodyRange.with({ end: new Position(funcEndLine, funcEndChar) });
×
276
                }
277
                currentFunction = new BrightScriptDeclaration(
×
278
                    match[1].trim(),
279
                    match[1].trim().toLowerCase() === 'new' ? SymbolKind.Constructor : SymbolKind.Function,
×
280
                    container,
281
                    match[2].split(','),
282
                    new Range(line, match[0].length - match[1].length - match[2].length - 2, line, match[0].length - 1),
283
                    new Range(line, 0, line, text.length)
284
                );
285
                symbols.push(currentFunction);
×
286

287
                if (classSymbol) {
×
288
                    currentFunction.container = classSymbol;
×
289
                } else if (namespaceSymbol) {
×
290
                    currentFunction.container = namespaceSymbol;
×
291
                }
292
                continue;
×
293
            }
294

295
            //FUNCTION END
296
            match = /^\s*(end)\s*(function|sub)/i.exec(text);
×
297
            if (match !== null) {
×
298
                // console.log("function END");
299
                if (currentFunction !== undefined) {
×
300
                    currentFunction.bodyRange = currentFunction.bodyRange.with({ end: new Position(funcEndLine, funcEndChar) });
×
301
                }
302
                continue;
×
303
            }
304

305
            // //FIELD
306
            match = /^(?!.*\()(?: |\t)*(public|private|protected)(?: |\t)*([a-z|\.|_]*).*((?: |\t)*=(?: |\t)*.*)*$/i.exec(text);
×
307
            if (match !== null) {
×
308
                // console.log("FOUND VAR " + match);
309
                const name = match[2].trim();
×
310
                if (mDefs[name] !== true) {
×
311
                    mDefs[name] = true;
×
312
                    let varSymbol = new BrightScriptDeclaration(
×
313
                        name,
314
                        SymbolKind.Field,
315
                        container,
316
                        undefined,
317
                        new Range(line, match[0].length - match[1].length, line, match[0].length),
318
                        new Range(line, 0, line, text.length)
319
                    );
320
                    console.log('FOUND VAR ' + varSymbol.name);
×
321
                    symbols.push(varSymbol);
×
322

323
                    if (classSymbol) {
×
324
                        varSymbol.container = classSymbol;
×
325
                    } else if (namespaceSymbol) {
×
326
                        varSymbol.container = namespaceSymbol;
×
327
                    }
328
                }
329
                continue;
×
330
            }
331

332
            //start namespace declaration
333
            match = /^(?: |\t)*namespace(?: |\t)*([a-z|\.|_]*).*$/i.exec(text);
×
334
            if (match !== null) {
×
335
                const name = match[1].trim();
×
336
                if (name) {
×
337
                    namespaceSymbol = new BrightScriptDeclaration(
×
338
                        name,
339
                        SymbolKind.Namespace,
340
                        container,
341
                        undefined,
342
                        new Range(line, match[0].length - match[1].length, line, match[0].length),
343
                        new Range(line, 0, line, text.length)
344
                    );
345
                    // console.log('FOUND NAMESPACES ' + namespaceSymbol.name);
346
                    symbols.push(namespaceSymbol);
×
347
                    namespaces.add(name.toLowerCase());
×
348
                }
349
            }
350
            //end namespace declaration
351
            match = /^(?: |\t)*end namespace.*$/i.exec(text);
×
352
            if (match !== null && namespaceSymbol) {
×
353
                namespaceSymbol = null;
×
354
            }
355

356
            //start enum declaration
357
            match = /^(?: |\t)*enum(?: |\t)*([a-z|\.|_]*).*$/i.exec(text);
×
358
            if (match !== null) {
×
359
                const name = match[1].trim();
×
360
                if (name) {
×
361
                    enumSymbol = new BrightScriptDeclaration(
×
362
                        name,
363
                        SymbolKind.Enum,
364
                        container,
365
                        undefined,
366
                        new Range(line, match[0].length - match[1].length, line, match[0].length),
367
                        new Range(line, 0, line, text.length)
368
                    );
369
                    // console.log('FOUND enumS ' + enumSymbol.name);
370
                    symbols.push(enumSymbol);
×
371
                    enums.add(name.toLowerCase());
×
372
                }
373
            }
374
            //end enum declaration
375
            match = /^(?: |\t)*end enum.*$/i.exec(text);
×
376
            if (match !== null && enumSymbol) {
×
377
                enumSymbol = null;
×
378
            }
379

380
            //start class declaration
381
            match = /(?:(class)\s+([a-z_][a-z0-9_]*))\s*(?:extends\s*([a-z_][a-z0-9_]+))*$/i.exec(text);
×
382
            if (match !== null) {
×
383
                const name = match[2].trim();
×
384
                if (name) {
×
385
                    classSymbol = new BrightScriptDeclaration(
×
386
                        name,
387
                        SymbolKind.Class,
388
                        container,
389
                        undefined,
390
                        new Range(line, match[0].length - match[2].length, line, match[0].length),
391
                        new Range(line, 0, line, text.length)
392
                    );
393
                    // console.log('FOUND CLASS ' + classSymbol.name);
394
                    symbols.push(classSymbol);
×
395
                    classes.add(name.toLowerCase());
×
396
                }
397
            }
398
            //start interface declaration
399
            match = /(?:(interface)\s+([a-z_][a-z0-9_]*))\s*(?:extends\s*([a-z_][a-z0-9_]+))*$/i.exec(text);
×
400
            if (match !== null) {
×
401
                const name = match[2].trim();
×
402
                if (name) {
×
403
                    interfaceSymbol = new BrightScriptDeclaration(
×
404
                        name,
405
                        SymbolKind.Interface,
406
                        container,
407
                        undefined,
408
                        new Range(line, match[0].length - match[2].length, line, match[0].length),
409
                        new Range(line, 0, line, text.length)
410
                    );
411
                    // console.log('FOUND interface ' + interfaceSymbol.name);
412
                    symbols.push(interfaceSymbol);
×
413
                    interfaces.add(name.toLowerCase());
×
414
                }
415
            }
416

417
        }
418
        this.fileNamespaces.set(uri, namespaces);
×
419
        this.fileClasses.set(uri, classes);
×
420
        this.cache.set(uri.fsPath, symbols);
×
421
        return symbols;
×
422
    }
423

424
    public declToSymbolInformation(uri: Uri, decl: BrightScriptDeclaration): SymbolInformation {
425
        return new SymbolInformation(
×
426
            decl.name,
427
            decl.kind,
428
            decl.containerName ? decl.containerName : decl.name,
×
429
            new Location(uri, decl.bodyRange)
430
        );
431
    }
432

433
    public getFunctionBeforeLine(filePath: string, lineNumber: number): BrightScriptDeclaration | null {
434
        let symbols = this.cache.get(filePath);
×
435
        if (!symbols) {
×
436
            try {
×
437

438
                let uri = vscode.Uri.file(filePath);
×
439
                let decls = this.readDeclarations(uri, fs.readFileSync(filePath, 'utf8'));
×
440
                this.cache.set(filePath, decls);
×
441
                // if there was no match, then get the declarations now
442
                symbols = this.cache.get(filePath);
×
443
            } catch (e) {
444
                console.error(`error loading symbols for file ${filePath}: ${e.message}`);
×
445
            }
446
        }
447
        //try to load it now
448
        if (symbols) {
×
449
            const matchingMethods = symbols
×
450
                .filter((symbol) => symbol.kind === SymbolKind.Function && symbol.nameRange.start.line < lineNumber);
×
451
            return matchingMethods.length > 0 ? matchingMethods[matchingMethods.length - 1] : null;
×
452
        }
453
        return null;
×
454
    }
455

456
}
457

458
export function getExcludeGlob(): string {
1✔
459
    const exclude = [
55✔
460
        ...Object.keys(vscode.workspace.getConfiguration('search', null).get('exclude') || {}),
110✔
461
        ...Object.keys(vscode.workspace.getConfiguration('files', null).get('exclude') || {})
110✔
462
    ].join(',');
463
    return `{${exclude}}`;
55✔
464
}
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

© 2025 Coveralls, Inc