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

rokucommunity / brighterscript / #13768

16 Jan 2024 07:10PM UTC coverage: 88.101% (+0.05%) from 88.056%
#13768

push

TwitchBronBron
0.65.17

5759 of 7011 branches covered (82.14%)

Branch coverage included in aggregate %.

8612 of 9301 relevant lines covered (92.59%)

1666.09 hits per line

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

93.23
/src/Program.ts
1
import * as assert from 'assert';
1✔
2
import * as fsExtra from 'fs-extra';
1✔
3
import * as path from 'path';
1✔
4
import type { CodeAction, CompletionItem, Position, Range, SignatureInformation, Location } from 'vscode-languageserver';
5
import { CompletionItemKind } from 'vscode-languageserver';
1✔
6
import type { BsConfig } from './BsConfig';
7
import { Scope } from './Scope';
1✔
8
import { DiagnosticMessages } from './DiagnosticMessages';
1✔
9
import { BrsFile } from './files/BrsFile';
1✔
10
import { XmlFile } from './files/XmlFile';
1✔
11
import type { BsDiagnostic, File, FileReference, FileObj, BscFile, SemanticToken, AfterFileTranspileEvent, FileLink, ProvideHoverEvent, ProvideCompletionsEvent, Hover } from './interfaces';
12
import { standardizePath as s, util } from './util';
1✔
13
import { XmlScope } from './XmlScope';
1✔
14
import { DiagnosticFilterer } from './DiagnosticFilterer';
1✔
15
import { DependencyGraph } from './DependencyGraph';
1✔
16
import { Logger, LogLevel } from './Logger';
1✔
17
import chalk from 'chalk';
1✔
18
import { globalFile } from './globalCallables';
1✔
19
import { parseManifest, getBsConst } from './preprocessor/Manifest';
1✔
20
import { URI } from 'vscode-uri';
1✔
21
import PluginInterface from './PluginInterface';
1✔
22
import { isBrsFile, isXmlFile, isXmlScope, isNamespaceStatement } from './astUtils/reflection';
1✔
23
import type { FunctionStatement, NamespaceStatement } from './parser/Statement';
24
import { BscPlugin } from './bscPlugin/BscPlugin';
1✔
25
import { AstEditor } from './astUtils/AstEditor';
1✔
26
import type { SourceMapGenerator } from 'source-map';
27
import { rokuDeploy } from 'roku-deploy';
1✔
28
import type { Statement } from './parser/AstNode';
29
import { CallExpressionInfo } from './bscPlugin/CallExpressionInfo';
1✔
30
import { SignatureHelpUtil } from './bscPlugin/SignatureHelpUtil';
1✔
31
import { DiagnosticSeverityAdjuster } from './DiagnosticSeverityAdjuster';
1✔
32

33
const startOfSourcePkgPath = `source${path.sep}`;
1✔
34
const bslibNonAliasedRokuModulesPkgPath = s`source/roku_modules/rokucommunity_bslib/bslib.brs`;
1✔
35
const bslibAliasedRokuModulesPkgPath = s`source/roku_modules/bslib/bslib.brs`;
1✔
36

37
export interface SourceObj {
38
    /**
39
     * @deprecated use `srcPath` instead
40
     */
41
    pathAbsolute: string;
42
    srcPath: string;
43
    source: string;
44
    definitions?: string;
45
}
46

47
export interface TranspileObj {
48
    file: BscFile;
49
    outputPath: string;
50
}
51

52
export interface SignatureInfoObj {
53
    index: number;
54
    key: string;
55
    signature: SignatureInformation;
56
}
57

58
export class Program {
1✔
59
    constructor(
60
        /**
61
         * The root directory for this program
62
         */
63
        public options: BsConfig,
1,058✔
64
        logger?: Logger,
65
        plugins?: PluginInterface
66
    ) {
67
        this.options = util.normalizeConfig(options);
1,058✔
68
        this.logger = logger || new Logger(options.logLevel as LogLevel);
1,058✔
69
        this.plugins = plugins || new PluginInterface([], { logger: this.logger });
1,058✔
70

71
        //inject the bsc plugin as the first plugin in the stack.
72
        this.plugins.addFirst(new BscPlugin());
1,058✔
73

74
        //normalize the root dir path
75
        this.options.rootDir = util.getRootDir(this.options);
1,058✔
76

77
        this.createGlobalScope();
1,058✔
78
    }
79

80
    public logger: Logger;
81

82
    private createGlobalScope() {
83
        //create the 'global' scope
84
        this.globalScope = new Scope('global', this, 'scope:global');
1,058✔
85
        this.globalScope.attachDependencyGraph(this.dependencyGraph);
1,058✔
86
        this.scopes.global = this.globalScope;
1,058✔
87
        //hardcode the files list for global scope to only contain the global file
88
        this.globalScope.getAllFiles = () => [globalFile];
16,138✔
89
        this.globalScope.validate();
1,058✔
90
        //for now, disable validation of global scope because the global files have some duplicate method declarations
91
        this.globalScope.getDiagnostics = () => [];
1,058✔
92
        //TODO we might need to fix this because the isValidated clears stuff now
93
        (this.globalScope as any).isValidated = true;
1,058✔
94
    }
95

96
    /**
97
     * A graph of all files and their dependencies.
98
     * For example:
99
     *      File.xml -> [lib1.brs, lib2.brs]
100
     *      lib2.brs -> [lib3.brs] //via an import statement
101
     */
102
    private dependencyGraph = new DependencyGraph();
1,058✔
103

104
    private diagnosticFilterer = new DiagnosticFilterer();
1,058✔
105

106
    private diagnosticAdjuster = new DiagnosticSeverityAdjuster();
1,058✔
107

108
    /**
109
     * A scope that contains all built-in global functions.
110
     * All scopes should directly or indirectly inherit from this scope
111
     */
112
    public globalScope: Scope;
113

114
    /**
115
     * Plugins which can provide extra diagnostics or transform AST
116
     */
117
    public plugins: PluginInterface;
118

119
    /**
120
     * A set of diagnostics. This does not include any of the scope diagnostics.
121
     * Should only be set from `this.validate()`
122
     */
123
    private diagnostics = [] as BsDiagnostic[];
1,058✔
124

125
    /**
126
     * The path to bslib.brs (the BrightScript runtime for certain BrighterScript features)
127
     */
128
    public get bslibPkgPath() {
129
        //if there's an aliased (preferred) version of bslib from roku_modules loaded into the program, use that
130
        if (this.getFile(bslibAliasedRokuModulesPkgPath)) {
375✔
131
            return bslibAliasedRokuModulesPkgPath;
2✔
132

133
            //if there's a non-aliased version of bslib from roku_modules, use that
134
        } else if (this.getFile(bslibNonAliasedRokuModulesPkgPath)) {
373✔
135
            return bslibNonAliasedRokuModulesPkgPath;
3✔
136

137
            //default to the embedded version
138
        } else {
139
            return `${this.options.bslibDestinationDir}${path.sep}bslib.brs`;
370✔
140
        }
141
    }
142

143
    public get bslibPrefix() {
144
        if (this.bslibPkgPath === bslibNonAliasedRokuModulesPkgPath) {
348✔
145
            return 'rokucommunity_bslib';
3✔
146
        } else {
147
            return 'bslib';
345✔
148
        }
149
    }
150

151

152
    /**
153
     * A map of every file loaded into this program, indexed by its original file location
154
     */
155
    public files = {} as Record<string, BscFile>;
1,058✔
156
    private pkgMap = {} as Record<string, BscFile>;
1,058✔
157

158
    private scopes = {} as Record<string, Scope>;
1,058✔
159

160
    protected addScope(scope: Scope) {
161
        this.scopes[scope.name] = scope;
959✔
162
    }
163

164
    /**
165
     * A map of every component currently loaded into the program, indexed by the component name.
166
     * It is a compile-time error to have multiple components with the same name. However, we store an array of components
167
     * by name so we can provide a better developer expreience. You shouldn't be directly accessing this array,
168
     * but if you do, only ever use the component at index 0.
169
     */
170
    private components = {} as Record<string, { file: XmlFile; scope: XmlScope }[]>;
1,058✔
171

172
    /**
173
     * Get the component with the specified name
174
     */
175
    public getComponent(componentName: string) {
176
        if (componentName) {
483✔
177
            //return the first compoment in the list with this name
178
            //(components are ordered in this list by pkgPath to ensure consistency)
179
            return this.components[componentName.toLowerCase()]?.[0];
465✔
180
        } else {
181
            return undefined;
18✔
182
        }
183
    }
184

185
    /**
186
     * Register (or replace) the reference to a component in the component map
187
     */
188
    private registerComponent(xmlFile: XmlFile, scope: XmlScope) {
189
        const key = (xmlFile.componentName?.text ?? xmlFile.pkgPath).toLowerCase();
204✔
190
        if (!this.components[key]) {
204✔
191
            this.components[key] = [];
192✔
192
        }
193
        this.components[key].push({
204✔
194
            file: xmlFile,
195
            scope: scope
196
        });
197
        this.components[key].sort((a, b) => {
204✔
198
            const pathA = a.file.pkgPath.toLowerCase();
5✔
199
            const pathB = b.file.pkgPath.toLowerCase();
5✔
200
            if (pathA < pathB) {
5✔
201
                return -1;
1✔
202
            } else if (pathA > pathB) {
4!
203
                return 1;
4✔
204
            }
205
            return 0;
×
206
        });
207
        this.syncComponentDependencyGraph(this.components[key]);
204✔
208
    }
209

210
    /**
211
     * Remove the specified component from the components map
212
     */
213
    private unregisterComponent(xmlFile: XmlFile) {
214
        const key = (xmlFile.componentName?.text ?? xmlFile.pkgPath).toLowerCase();
11✔
215
        const arr = this.components[key] || [];
11!
216
        for (let i = 0; i < arr.length; i++) {
11✔
217
            if (arr[i].file === xmlFile) {
11!
218
                arr.splice(i, 1);
11✔
219
                break;
11✔
220
            }
221
        }
222
        this.syncComponentDependencyGraph(arr);
11✔
223
    }
224

225
    /**
226
     * re-attach the dependency graph with a new key for any component who changed
227
     * their position in their own named array (only matters when there are multiple
228
     * components with the same name)
229
     */
230
    private syncComponentDependencyGraph(components: Array<{ file: XmlFile; scope: XmlScope }>) {
231
        //reattach every dependency graph
232
        for (let i = 0; i < components.length; i++) {
215✔
233
            const { file, scope } = components[i];
210✔
234

235
            //attach (or re-attach) the dependencyGraph for every component whose position changed
236
            if (file.dependencyGraphIndex !== i) {
210✔
237
                file.dependencyGraphIndex = i;
206✔
238
                file.attachDependencyGraph(this.dependencyGraph);
206✔
239
                scope.attachDependencyGraph(this.dependencyGraph);
206✔
240
            }
241
        }
242
    }
243

244
    /**
245
     * Get a list of all files that are included in the project but are not referenced
246
     * by any scope in the program.
247
     */
248
    public getUnreferencedFiles() {
249
        let result = [] as File[];
629✔
250
        for (let filePath in this.files) {
629✔
251
            let file = this.files[filePath];
808✔
252
            //is this file part of a scope
253
            if (!this.getFirstScopeForFile(file)) {
808✔
254
                //no scopes reference this file. add it to the list
255
                result.push(file);
20✔
256
            }
257
        }
258
        return result;
629✔
259
    }
260

261
    /**
262
     * Get the list of errors for the entire program. It's calculated on the fly
263
     * by walking through every file, so call this sparingly.
264
     */
265
    public getDiagnostics() {
266
        return this.logger.time(LogLevel.info, ['Program.getDiagnostics()'], () => {
629✔
267

268
            let diagnostics = [...this.diagnostics];
629✔
269

270
            //get the diagnostics from all scopes
271
            for (let scopeName in this.scopes) {
629✔
272
                let scope = this.scopes[scopeName];
1,308✔
273
                diagnostics.push(
1,308✔
274
                    ...scope.getDiagnostics()
275
                );
276
            }
277

278
            //get the diagnostics from all unreferenced files
279
            let unreferencedFiles = this.getUnreferencedFiles();
629✔
280
            for (let file of unreferencedFiles) {
629✔
281
                diagnostics.push(
20✔
282
                    ...file.getDiagnostics()
283
                );
284
            }
285
            const filteredDiagnostics = this.logger.time(LogLevel.debug, ['filter diagnostics'], () => {
629✔
286
                //filter out diagnostics based on our diagnostic filters
287
                let finalDiagnostics = this.diagnosticFilterer.filter({
629✔
288
                    ...this.options,
289
                    rootDir: this.options.rootDir
290
                }, diagnostics);
291
                return finalDiagnostics;
629✔
292
            });
293

294
            this.logger.time(LogLevel.debug, ['adjust diagnostics severity'], () => {
629✔
295
                this.diagnosticAdjuster.adjust(this.options, diagnostics);
629✔
296
            });
297

298
            this.logger.info(`diagnostic counts: total=${chalk.yellow(diagnostics.length.toString())}, after filter=${chalk.yellow(filteredDiagnostics.length.toString())}`);
629✔
299
            return filteredDiagnostics;
629✔
300
        });
301
    }
302

303
    public addDiagnostics(diagnostics: BsDiagnostic[]) {
304
        this.diagnostics.push(...diagnostics);
12✔
305
    }
306

307
    /**
308
     * Determine if the specified file is loaded in this program right now.
309
     * @param filePath the absolute or relative path to the file
310
     * @param normalizePath should the provided path be normalized before use
311
     */
312
    public hasFile(filePath: string, normalizePath = true) {
1,163✔
313
        return !!this.getFile(filePath, normalizePath);
1,163✔
314
    }
315

316
    public getPkgPath(...args: any[]): any { //eslint-disable-line
317
        throw new Error('Not implemented');
×
318
    }
319

320
    /**
321
     * roku filesystem is case INsensitive, so find the scope by key case insensitive
322
     */
323
    public getScopeByName(scopeName: string) {
324
        if (!scopeName) {
35!
325
            return undefined;
×
326
        }
327
        //most scopes are xml file pkg paths. however, the ones that are not are single names like "global" and "scope",
328
        //so it's safe to run the standardizePkgPath method
329
        scopeName = s`${scopeName}`;
35✔
330
        let key = Object.keys(this.scopes).find(x => x.toLowerCase() === scopeName.toLowerCase());
86✔
331
        return this.scopes[key];
35✔
332
    }
333

334
    /**
335
     * Return all scopes
336
     */
337
    public getScopes() {
338
        return Object.values(this.scopes);
7✔
339
    }
340

341
    /**
342
     * Find the scope for the specified component
343
     */
344
    public getComponentScope(componentName: string) {
345
        return this.getComponent(componentName)?.scope;
138✔
346
    }
347

348
    /**
349
     * Update internal maps with this file reference
350
     */
351
    private assignFile<T extends BscFile = BscFile>(file: T) {
352
        this.files[file.srcPath.toLowerCase()] = file;
1,145✔
353
        this.pkgMap[file.pkgPath.toLowerCase()] = file;
1,145✔
354
        return file;
1,145✔
355
    }
356

357
    /**
358
     * Remove this file from internal maps
359
     */
360
    private unassignFile<T extends BscFile = BscFile>(file: T) {
361
        delete this.files[file.srcPath.toLowerCase()];
76✔
362
        delete this.pkgMap[file.pkgPath.toLowerCase()];
76✔
363
        return file;
76✔
364
    }
365

366
    /**
367
     * Load a file into the program. If that file already exists, it is replaced.
368
     * If file contents are provided, those are used, Otherwise, the file is loaded from the file system
369
     * @param srcPath the file path relative to the root dir
370
     * @param fileContents the file contents
371
     * @deprecated use `setFile` instead
372
     */
373
    public addOrReplaceFile<T extends BscFile>(srcPath: string, fileContents: string): T;
374
    /**
375
     * Load a file into the program. If that file already exists, it is replaced.
376
     * @param fileEntry an object that specifies src and dest for the file.
377
     * @param fileContents the file contents. If not provided, the file will be loaded from disk
378
     * @deprecated use `setFile` instead
379
     */
380
    public addOrReplaceFile<T extends BscFile>(fileEntry: FileObj, fileContents: string): T;
381
    public addOrReplaceFile<T extends BscFile>(fileParam: FileObj | string, fileContents: string): T {
382
        return this.setFile<T>(fileParam as any, fileContents);
1✔
383
    }
384

385
    /**
386
     * Load a file into the program. If that file already exists, it is replaced.
387
     * If file contents are provided, those are used, Otherwise, the file is loaded from the file system
388
     * @param srcDestOrPkgPath the absolute path, the pkg path (i.e. `pkg:/path/to/file.brs`), or the destPath (i.e. `path/to/file.brs` relative to `pkg:/`)
389
     * @param fileContents the file contents
390
     */
391
    public setFile<T extends BscFile>(srcDestOrPkgPath: string, fileContents: string): T;
392
    /**
393
     * Load a file into the program. If that file already exists, it is replaced.
394
     * @param fileEntry an object that specifies src and dest for the file.
395
     * @param fileContents the file contents. If not provided, the file will be loaded from disk
396
     */
397
    public setFile<T extends BscFile>(fileEntry: FileObj, fileContents: string): T;
398
    public setFile<T extends BscFile>(fileParam: FileObj | string, fileContents: string): T {
399
        //normalize the file paths
400
        const { srcPath, pkgPath } = this.getPaths(fileParam, this.options.rootDir);
1,148✔
401

402
        let file = this.logger.time(LogLevel.debug, ['Program.setFile()', chalk.green(srcPath)], () => {
1,148✔
403
            //if the file is already loaded, remove it
404
            if (this.hasFile(srcPath)) {
1,148✔
405
                this.removeFile(srcPath);
66✔
406
            }
407
            let fileExtension = path.extname(srcPath).toLowerCase();
1,148✔
408
            let file: BscFile | undefined;
409

410
            if (fileExtension === '.brs' || fileExtension === '.bs') {
1,148✔
411
                //add the file to the program
412
                const brsFile = this.assignFile(
941✔
413
                    new BrsFile(srcPath, pkgPath, this)
414
                );
415

416
                //add file to the `source` dependency list
417
                if (brsFile.pkgPath.startsWith(startOfSourcePkgPath)) {
941✔
418
                    this.createSourceScope();
805✔
419
                    this.dependencyGraph.addDependency('scope:source', brsFile.dependencyGraphKey);
805✔
420
                }
421

422
                let sourceObj: SourceObj = {
941✔
423
                    //TODO remove `pathAbsolute` in v1
424
                    pathAbsolute: srcPath,
425
                    srcPath: srcPath,
426
                    source: fileContents
427
                };
428
                this.plugins.emit('beforeFileParse', sourceObj);
941✔
429

430
                this.logger.time(LogLevel.debug, ['parse', chalk.green(srcPath)], () => {
941✔
431
                    brsFile.parse(sourceObj.source);
941✔
432
                });
433

434
                //notify plugins that this file has finished parsing
435
                this.plugins.emit('afterFileParse', brsFile);
941✔
436

437
                file = brsFile;
941✔
438

439
                brsFile.attachDependencyGraph(this.dependencyGraph);
941✔
440

441
            } else if (
207✔
442
                //is xml file
443
                fileExtension === '.xml' &&
414✔
444
                //resides in the components folder (Roku will only parse xml files in the components folder)
445
                pkgPath.toLowerCase().startsWith(util.pathSepNormalize(`components/`))
446
            ) {
447
                //add the file to the program
448
                const xmlFile = this.assignFile(
204✔
449
                    new XmlFile(srcPath, pkgPath, this)
450
                );
451

452
                let sourceObj: SourceObj = {
204✔
453
                    //TODO remove `pathAbsolute` in v1
454
                    pathAbsolute: srcPath,
455
                    srcPath: srcPath,
456
                    source: fileContents
457
                };
458
                this.plugins.emit('beforeFileParse', sourceObj);
204✔
459

460
                this.logger.time(LogLevel.debug, ['parse', chalk.green(srcPath)], () => {
204✔
461
                    xmlFile.parse(sourceObj.source);
204✔
462
                });
463

464
                //notify plugins that this file has finished parsing
465
                this.plugins.emit('afterFileParse', xmlFile);
204✔
466

467
                file = xmlFile;
204✔
468

469
                //create a new scope for this xml file
470
                let scope = new XmlScope(xmlFile, this);
204✔
471
                this.addScope(scope);
204✔
472

473
                //register this compoent now that we have parsed it and know its component name
474
                this.registerComponent(xmlFile, scope);
204✔
475

476
                //notify plugins that the scope is created and the component is registered
477
                this.plugins.emit('afterScopeCreate', scope);
204✔
478
            } else {
479
                //TODO do we actually need to implement this? Figure out how to handle img paths
480
                // let genericFile = this.files[srcPath] = <any>{
481
                //     srcPath: srcPath,
482
                //     pkgPath: pkgPath,
483
                //     wasProcessed: true
484
                // } as File;
485
                // file = <any>genericFile;
486
            }
487
            return file;
1,148✔
488
        });
489
        return file as T;
1,148✔
490
    }
491

492
    /**
493
     * Given a srcPath, a pkgPath, or both, resolve whichever is missing, relative to rootDir.
494
     * @param fileParam an object representing file paths
495
     * @param rootDir must be a pre-normalized path
496
     */
497
    private getPaths(fileParam: string | FileObj | { srcPath?: string; pkgPath?: string }, rootDir: string) {
498
        let srcPath: string;
499
        let pkgPath: string;
500

501
        assert.ok(fileParam, 'fileParam is required');
1,155✔
502

503
        //lift the srcPath and pkgPath vars from the incoming param
504
        if (typeof fileParam === 'string') {
1,155✔
505
            fileParam = this.removePkgPrefix(fileParam);
889✔
506
            srcPath = s`${path.resolve(rootDir, fileParam)}`;
889✔
507
            pkgPath = s`${util.replaceCaseInsensitive(srcPath, rootDir, '')}`;
889✔
508
        } else {
509
            let param: any = fileParam;
266✔
510

511
            if (param.src) {
266✔
512
                srcPath = s`${param.src}`;
263✔
513
            }
514
            if (param.srcPath) {
266✔
515
                srcPath = s`${param.srcPath}`;
2✔
516
            }
517
            if (param.dest) {
266✔
518
                pkgPath = s`${this.removePkgPrefix(param.dest)}`;
263✔
519
            }
520
            if (param.pkgPath) {
266✔
521
                pkgPath = s`${this.removePkgPrefix(param.pkgPath)}`;
2✔
522
            }
523
        }
524

525
        //if there's no srcPath, use the pkgPath to build an absolute srcPath
526
        if (!srcPath) {
1,155✔
527
            srcPath = s`${rootDir}/${pkgPath}`;
1✔
528
        }
529
        //coerce srcPath to an absolute path
530
        if (!path.isAbsolute(srcPath)) {
1,155✔
531
            srcPath = util.standardizePath(srcPath);
1✔
532
        }
533

534
        //if there's no pkgPath, compute relative path from rootDir
535
        if (!pkgPath) {
1,155✔
536
            pkgPath = s`${util.replaceCaseInsensitive(srcPath, rootDir, '')}`;
1✔
537
        }
538

539
        assert.ok(srcPath, 'fileEntry.src is required');
1,155✔
540
        assert.ok(pkgPath, 'fileEntry.dest is required');
1,155✔
541

542
        return {
1,155✔
543
            srcPath: srcPath,
544
            //remove leading slash from pkgPath
545
            pkgPath: pkgPath.replace(/^[\/\\]+/, '')
546
        };
547
    }
548

549
    /**
550
     * Remove any leading `pkg:/` found in the path
551
     */
552
    private removePkgPrefix(path: string) {
553
        return path.replace(/^pkg:\//i, '');
1,154✔
554
    }
555

556
    /**
557
     * Ensure source scope is created.
558
     * Note: automatically called internally, and no-op if it exists already.
559
     */
560
    public createSourceScope() {
561
        if (!this.scopes.source) {
1,058✔
562
            const sourceScope = new Scope('source', this, 'scope:source');
755✔
563
            sourceScope.attachDependencyGraph(this.dependencyGraph);
755✔
564
            this.addScope(sourceScope);
755✔
565
            this.plugins.emit('afterScopeCreate', sourceScope);
755✔
566
        }
567
    }
568

569
    /**
570
     * Find the file by its absolute path. This is case INSENSITIVE, since
571
     * Roku is a case insensitive file system. It is an error to have multiple files
572
     * with the same path with only case being different.
573
     * @param srcPath the absolute path to the file
574
     * @deprecated use `getFile` instead, which auto-detects the path type
575
     */
576
    public getFileByPathAbsolute<T extends BrsFile | XmlFile>(srcPath: string) {
577
        srcPath = s`${srcPath}`;
×
578
        for (let filePath in this.files) {
×
579
            if (filePath.toLowerCase() === srcPath.toLowerCase()) {
×
580
                return this.files[filePath] as T;
×
581
            }
582
        }
583
    }
584

585
    /**
586
     * Get a list of files for the given (platform-normalized) pkgPath array.
587
     * Missing files are just ignored.
588
     * @deprecated use `getFiles` instead, which auto-detects the path types
589
     */
590
    public getFilesByPkgPaths<T extends BscFile[]>(pkgPaths: string[]) {
591
        return pkgPaths
×
592
            .map(pkgPath => this.getFileByPkgPath(pkgPath))
×
593
            .filter(file => file !== undefined) as T;
×
594
    }
595

596
    /**
597
     * Get a file with the specified (platform-normalized) pkg path.
598
     * If not found, return undefined
599
     * @deprecated use `getFile` instead, which auto-detects the path type
600
     */
601
    public getFileByPkgPath<T extends BscFile>(pkgPath: string) {
602
        return this.pkgMap[pkgPath.toLowerCase()] as T;
345✔
603
    }
604

605
    /**
606
     * Remove a set of files from the program
607
     * @param srcPaths can be an array of srcPath or destPath strings
608
     * @param normalizePath should this function repair and standardize the filePaths? Passing false should have a performance boost if you can guarantee your paths are already sanitized
609
     */
610
    public removeFiles(srcPaths: string[], normalizePath = true) {
1✔
611
        for (let srcPath of srcPaths) {
1✔
612
            this.removeFile(srcPath, normalizePath);
1✔
613
        }
614
    }
615

616
    /**
617
     * Remove a file from the program
618
     * @param filePath can be a srcPath, a pkgPath, or a destPath (same as pkgPath but without `pkg:/`)
619
     * @param normalizePath should this function repair and standardize the path? Passing false should have a performance boost if you can guarantee your path is already sanitized
620
     */
621
    public removeFile(filePath: string, normalizePath = true) {
75✔
622
        this.logger.debug('Program.removeFile()', filePath);
76✔
623

624
        let file = this.getFile(filePath, normalizePath);
76✔
625
        if (file) {
76!
626
            this.plugins.emit('beforeFileDispose', file);
76✔
627

628
            //if there is a scope named the same as this file's path, remove it (i.e. xml scopes)
629
            let scope = this.scopes[file.pkgPath];
76✔
630
            if (scope) {
76✔
631
                this.plugins.emit('beforeScopeDispose', scope);
11✔
632
                scope.dispose();
11✔
633
                //notify dependencies of this scope that it has been removed
634
                this.dependencyGraph.remove(scope.dependencyGraphKey);
11✔
635
                delete this.scopes[file.pkgPath];
11✔
636
                this.plugins.emit('afterScopeDispose', scope);
11✔
637
            }
638
            //remove the file from the program
639
            this.unassignFile(file);
76✔
640

641
            this.dependencyGraph.remove(file.dependencyGraphKey);
76✔
642

643
            //if this is a pkg:/source file, notify the `source` scope that it has changed
644
            if (file.pkgPath.startsWith(startOfSourcePkgPath)) {
76✔
645
                this.dependencyGraph.removeDependency('scope:source', file.dependencyGraphKey);
62✔
646
            }
647

648
            //if this is a component, remove it from our components map
649
            if (isXmlFile(file)) {
76✔
650
                this.unregisterComponent(file);
11✔
651
            }
652
            //dispose file
653
            file?.dispose();
76!
654
            this.plugins.emit('afterFileDispose', file);
76✔
655
        }
656
    }
657

658
    /**
659
     * Traverse the entire project, and validate all scopes
660
     */
661
    public validate() {
662
        this.logger.time(LogLevel.log, ['Validating project'], () => {
642✔
663
            this.diagnostics = [];
642✔
664
            this.plugins.emit('beforeProgramValidate', this);
642✔
665

666
            //validate every file
667
            for (const file of Object.values(this.files)) {
642✔
668
                //for every unvalidated file, validate it
669
                if (!file.isValidated) {
819✔
670
                    this.plugins.emit('beforeFileValidate', {
791✔
671
                        program: this,
672
                        file: file
673
                    });
674

675
                    //emit an event to allow plugins to contribute to the file validation process
676
                    this.plugins.emit('onFileValidate', {
791✔
677
                        program: this,
678
                        file: file
679
                    });
680
                    //call file.validate() IF the file has that function defined
681
                    file.validate?.();
791!
682
                    file.isValidated = true;
791✔
683

684
                    this.plugins.emit('afterFileValidate', file);
791✔
685
                }
686
            }
687

688
            this.logger.time(LogLevel.info, ['Validate all scopes'], () => {
642✔
689
                for (let scopeName in this.scopes) {
642✔
690
                    let scope = this.scopes[scopeName];
1,353✔
691
                    scope.linkSymbolTable();
1,353✔
692
                    scope.validate();
1,353✔
693
                    scope.unlinkSymbolTable();
1,353✔
694
                }
695
            });
696

697
            this.detectDuplicateComponentNames();
642✔
698

699
            this.plugins.emit('afterProgramValidate', this);
642✔
700
        });
701
    }
702

703
    /**
704
     * Flag all duplicate component names
705
     */
706
    private detectDuplicateComponentNames() {
707
        const componentsByName = Object.keys(this.files).reduce<Record<string, XmlFile[]>>((map, filePath) => {
642✔
708
            const file = this.files[filePath];
819✔
709
            //if this is an XmlFile, and it has a valid `componentName` property
710
            if (isXmlFile(file) && file.componentName?.text) {
819✔
711
                let lowerName = file.componentName.text.toLowerCase();
138✔
712
                if (!map[lowerName]) {
138✔
713
                    map[lowerName] = [];
135✔
714
                }
715
                map[lowerName].push(file);
138✔
716
            }
717
            return map;
819✔
718
        }, {});
719

720
        for (let name in componentsByName) {
642✔
721
            const xmlFiles = componentsByName[name];
135✔
722
            //add diagnostics for every duplicate component with this name
723
            if (xmlFiles.length > 1) {
135✔
724
                for (let xmlFile of xmlFiles) {
3✔
725
                    const { componentName } = xmlFile;
6✔
726
                    this.diagnostics.push({
6✔
727
                        ...DiagnosticMessages.duplicateComponentName(componentName.text),
728
                        range: xmlFile.componentName.range,
729
                        file: xmlFile,
730
                        relatedInformation: xmlFiles.filter(x => x !== xmlFile).map(x => {
12✔
731
                            return {
6✔
732
                                location: util.createLocation(
733
                                    URI.file(xmlFile.srcPath ?? xmlFile.srcPath).toString(),
18!
734
                                    x.componentName.range
735
                                ),
736
                                message: 'Also defined here'
737
                            };
738
                        })
739
                    });
740
                }
741
            }
742
        }
743
    }
744

745
    /**
746
     * Get the files for a list of filePaths
747
     * @param filePaths can be an array of srcPath or a destPath strings
748
     * @param normalizePath should this function repair and standardize the paths? Passing false should have a performance boost if you can guarantee your paths are already sanitized
749
     */
750
    public getFiles<T extends BscFile>(filePaths: string[], normalizePath = true) {
24✔
751
        return filePaths
24✔
752
            .map(filePath => this.getFile(filePath, normalizePath))
30✔
753
            .filter(file => file !== undefined) as T[];
30✔
754
    }
755

756
    /**
757
     * Get the file at the given path
758
     * @param filePath can be a srcPath or a destPath
759
     * @param normalizePath should this function repair and standardize the path? Passing false should have a performance boost if you can guarantee your path is already sanitized
760
     */
761
    public getFile<T extends BscFile>(filePath: string, normalizePath = true) {
4,718✔
762
        if (typeof filePath !== 'string') {
5,987✔
763
            return undefined;
1,351✔
764
        } else if (path.isAbsolute(filePath)) {
4,636✔
765
            return this.files[
2,329✔
766
                (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase()
2,329!
767
            ] as T;
768
        } else {
769
            return this.pkgMap[
2,307✔
770
                (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase()
2,307!
771
            ] as T;
772
        }
773
    }
774

775
    /**
776
     * Get a list of all scopes the file is loaded into
777
     * @param file the file
778
     */
779
    public getScopesForFile(file: XmlFile | BrsFile | string) {
780
        if (typeof file === 'string') {
626✔
781
            file = this.getFile(file);
3✔
782
        }
783
        let result = [] as Scope[];
626✔
784
        if (file) {
626✔
785
            for (let key in this.scopes) {
625✔
786
                let scope = this.scopes[key];
1,240✔
787

788
                if (scope.hasFile(file)) {
1,240✔
789
                    result.push(scope);
589✔
790
                }
791
            }
792
        }
793
        return result;
626✔
794
    }
795

796
    /**
797
     * Get the first found scope for a file.
798
     */
799
    public getFirstScopeForFile(file: XmlFile | BrsFile): Scope | undefined {
800
        for (let key in this.scopes) {
1,713✔
801
            let scope = this.scopes[key];
4,108✔
802

803
            if (scope.hasFile(file)) {
4,108✔
804
                return scope;
1,680✔
805
            }
806
        }
807
    }
808

809
    public getStatementsByName(name: string, originFile: BrsFile, namespaceName?: string): FileLink<Statement>[] {
810
        let results = new Map<Statement, FileLink<Statement>>();
38✔
811
        const filesSearched = new Set<BrsFile>();
38✔
812
        let lowerNamespaceName = namespaceName?.toLowerCase();
38✔
813
        let lowerName = name?.toLowerCase();
38!
814
        //look through all files in scope for matches
815
        for (const scope of this.getScopesForFile(originFile)) {
38✔
816
            for (const file of scope.getAllFiles()) {
38✔
817
                if (isXmlFile(file) || filesSearched.has(file)) {
44✔
818
                    continue;
3✔
819
                }
820
                filesSearched.add(file);
41✔
821

822
                for (const statement of [...file.parser.references.functionStatements, ...file.parser.references.classStatements.flatMap((cs) => cs.methods)]) {
41✔
823
                    let parentNamespaceName = statement.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(originFile.parseMode)?.toLowerCase();
97✔
824
                    if (statement.name.text.toLowerCase() === lowerName && (!lowerNamespaceName || parentNamespaceName === lowerNamespaceName)) {
97✔
825
                        if (!results.has(statement)) {
36!
826
                            results.set(statement, { item: statement, file: file });
36✔
827
                        }
828
                    }
829
                }
830
            }
831
        }
832
        return [...results.values()];
38✔
833
    }
834

835
    public getStatementsForXmlFile(scope: XmlScope, filterName?: string): FileLink<FunctionStatement>[] {
836
        let results = new Map<Statement, FileLink<FunctionStatement>>();
8✔
837
        const filesSearched = new Set<BrsFile>();
8✔
838

839
        //get all function names for the xml file and parents
840
        let funcNames = new Set<string>();
8✔
841
        let currentScope = scope;
8✔
842
        while (isXmlScope(currentScope)) {
8✔
843
            for (let name of currentScope.xmlFile.ast.component.api?.functions.map((f) => f.name) ?? []) {
14✔
844
                if (!filterName || name === filterName) {
14!
845
                    funcNames.add(name);
14✔
846
                }
847
            }
848
            currentScope = currentScope.getParentScope() as XmlScope;
10✔
849
        }
850

851
        //look through all files in scope for matches
852
        for (const file of scope.getOwnFiles()) {
8✔
853
            if (isXmlFile(file) || filesSearched.has(file)) {
16✔
854
                continue;
8✔
855
            }
856
            filesSearched.add(file);
8✔
857

858
            for (const statement of file.parser.references.functionStatements) {
8✔
859
                if (funcNames.has(statement.name.text)) {
13!
860
                    if (!results.has(statement)) {
13!
861
                        results.set(statement, { item: statement, file: file });
13✔
862
                    }
863
                }
864
            }
865
        }
866
        return [...results.values()];
8✔
867
    }
868

869
    /**
870
     * Find all available completion items at the given position
871
     * @param filePath can be a srcPath or a destPath
872
     * @param position the position (line & column) where completions should be found
873
     */
874
    public getCompletions(filePath: string, position: Position) {
875
        let file = this.getFile(filePath);
74✔
876
        if (!file) {
74!
877
            return [];
×
878
        }
879

880
        //find the scopes for this file
881
        let scopes = this.getScopesForFile(file);
74✔
882

883
        //if there are no scopes, include the global scope so we at least get the built-in functions
884
        scopes = scopes.length > 0 ? scopes : [this.globalScope];
74✔
885

886
        const event: ProvideCompletionsEvent = {
74✔
887
            program: this,
888
            file: file,
889
            scopes: scopes,
890
            position: position,
891
            completions: []
892
        };
893

894
        this.plugins.emit('beforeProvideCompletions', event);
74✔
895

896
        this.plugins.emit('provideCompletions', event);
74✔
897

898
        this.plugins.emit('afterProvideCompletions', event);
74✔
899

900
        return event.completions;
74✔
901
    }
902

903
    /**
904
     * Goes through each file and builds a list of workspace symbols for the program. Used by LanguageServer's onWorkspaceSymbol functionality
905
     */
906
    public getWorkspaceSymbols() {
907
        const results = Object.keys(this.files).map(key => {
4✔
908
            const file = this.files[key];
8✔
909
            if (isBrsFile(file)) {
8!
910
                return file.getWorkspaceSymbols();
8✔
911
            }
912
            return [];
×
913
        });
914
        return util.flatMap(results, c => c);
8✔
915
    }
916

917
    /**
918
     * Given a position in a file, if the position is sitting on some type of identifier,
919
     * go to the definition of that identifier (where this thing was first defined)
920
     */
921
    public getDefinition(srcPath: string, position: Position) {
922
        let file = this.getFile(srcPath);
9✔
923
        if (!file) {
9!
924
            return [];
×
925
        }
926

927
        if (isBrsFile(file)) {
9!
928
            return file.getDefinition(position);
9✔
929
        } else {
930
            let results = [] as Location[];
×
931
            const scopes = this.getScopesForFile(file);
×
932
            for (const scope of scopes) {
×
933
                results = results.concat(...scope.getDefinition(file, position));
×
934
            }
935
            return results;
×
936
        }
937
    }
938

939
    /**
940
     * Get hover information for a file and position
941
     */
942
    public getHover(srcPath: string, position: Position): Hover[] {
943
        let file = this.getFile(srcPath);
30✔
944
        let result: Hover[];
945
        if (file) {
30!
946
            const event = {
30✔
947
                program: this,
948
                file: file,
949
                position: position,
950
                scopes: this.getScopesForFile(file),
951
                hovers: []
952
            } as ProvideHoverEvent;
953
            this.plugins.emit('beforeProvideHover', event);
30✔
954
            this.plugins.emit('provideHover', event);
30✔
955
            this.plugins.emit('afterProvideHover', event);
30✔
956
            result = event.hovers;
30✔
957
        }
958

959
        return result ?? [];
30!
960
    }
961

962
    /**
963
     * Compute code actions for the given file and range
964
     */
965
    public getCodeActions(srcPath: string, range: Range) {
966
        const codeActions = [] as CodeAction[];
11✔
967
        const file = this.getFile(srcPath);
11✔
968
        if (file) {
11✔
969
            const diagnostics = this
10✔
970
                //get all current diagnostics (filtered by diagnostic filters)
971
                .getDiagnostics()
972
                //only keep diagnostics related to this file
973
                .filter(x => x.file === file)
26✔
974
                //only keep diagnostics that touch this range
975
                .filter(x => util.rangesIntersectOrTouch(x.range, range));
10✔
976

977
            const scopes = this.getScopesForFile(file);
10✔
978

979
            this.plugins.emit('onGetCodeActions', {
10✔
980
                program: this,
981
                file: file,
982
                range: range,
983
                diagnostics: diagnostics,
984
                scopes: scopes,
985
                codeActions: codeActions
986
            });
987
        }
988
        return codeActions;
11✔
989
    }
990

991
    /**
992
     * Get semantic tokens for the specified file
993
     */
994
    public getSemanticTokens(srcPath: string): SemanticToken[] | undefined {
995
        const file = this.getFile(srcPath);
16✔
996
        if (file) {
16!
997
            const result = [] as SemanticToken[];
16✔
998
            this.plugins.emit('onGetSemanticTokens', {
16✔
999
                program: this,
1000
                file: file,
1001
                scopes: this.getScopesForFile(file),
1002
                semanticTokens: result
1003
            });
1004
            return result;
16✔
1005
        }
1006
    }
1007

1008
    public getSignatureHelp(filepath: string, position: Position): SignatureInfoObj[] {
1009
        let file: BrsFile = this.getFile(filepath);
184✔
1010
        if (!file || !isBrsFile(file)) {
184✔
1011
            return [];
3✔
1012
        }
1013
        let callExpressionInfo = new CallExpressionInfo(file, position);
181✔
1014
        let signatureHelpUtil = new SignatureHelpUtil();
181✔
1015
        return signatureHelpUtil.getSignatureHelpItems(callExpressionInfo);
181✔
1016
    }
1017

1018
    public getReferences(srcPath: string, position: Position) {
1019
        //find the file
1020
        let file = this.getFile(srcPath);
3✔
1021
        if (!file) {
3!
1022
            return null;
×
1023
        }
1024

1025
        return file.getReferences(position);
3✔
1026
    }
1027

1028
    /**
1029
     * Get a list of all script imports, relative to the specified pkgPath
1030
     * @param sourcePkgPath - the pkgPath of the source that wants to resolve script imports.
1031
     */
1032
    public getScriptImportCompletions(sourcePkgPath: string, scriptImport: FileReference) {
1033
        let lowerSourcePkgPath = sourcePkgPath.toLowerCase();
3✔
1034

1035
        let result = [] as CompletionItem[];
3✔
1036
        /**
1037
         * hashtable to prevent duplicate results
1038
         */
1039
        let resultPkgPaths = {} as Record<string, boolean>;
3✔
1040

1041
        //restrict to only .brs files
1042
        for (let key in this.files) {
3✔
1043
            let file = this.files[key];
4✔
1044
            if (
4✔
1045
                //is a BrightScript or BrighterScript file
1046
                (file.extension === '.bs' || file.extension === '.brs') &&
11✔
1047
                //this file is not the current file
1048
                lowerSourcePkgPath !== file.pkgPath.toLowerCase()
1049
            ) {
1050
                //add the relative path
1051
                let relativePath = util.getRelativePath(sourcePkgPath, file.pkgPath).replace(/\\/g, '/');
3✔
1052
                let pkgPathStandardized = file.pkgPath.replace(/\\/g, '/');
3✔
1053
                let filePkgPath = `pkg:/${pkgPathStandardized}`;
3✔
1054
                let lowerFilePkgPath = filePkgPath.toLowerCase();
3✔
1055
                if (!resultPkgPaths[lowerFilePkgPath]) {
3!
1056
                    resultPkgPaths[lowerFilePkgPath] = true;
3✔
1057

1058
                    result.push({
3✔
1059
                        label: relativePath,
1060
                        detail: file.srcPath,
1061
                        kind: CompletionItemKind.File,
1062
                        textEdit: {
1063
                            newText: relativePath,
1064
                            range: scriptImport.filePathRange
1065
                        }
1066
                    });
1067

1068
                    //add the absolute path
1069
                    result.push({
3✔
1070
                        label: filePkgPath,
1071
                        detail: file.srcPath,
1072
                        kind: CompletionItemKind.File,
1073
                        textEdit: {
1074
                            newText: filePkgPath,
1075
                            range: scriptImport.filePathRange
1076
                        }
1077
                    });
1078
                }
1079
            }
1080
        }
1081
        return result;
3✔
1082
    }
1083

1084
    /**
1085
     * Transpile a single file and get the result as a string.
1086
     * This does not write anything to the file system.
1087
     *
1088
     * This should only be called by `LanguageServer`.
1089
     * Internal usage should call `_getTranspiledFileContents` instead.
1090
     * @param filePath can be a srcPath or a destPath
1091
     */
1092
    public async getTranspiledFileContents(filePath: string) {
1093
        let fileMap = await rokuDeploy.getFilePaths(this.options.files, this.options.rootDir);
4✔
1094
        //remove files currently loaded in the program, we will transpile those instead (even if just for source maps)
1095
        let filteredFileMap = [] as FileObj[];
4✔
1096
        for (let fileEntry of fileMap) {
4✔
1097
            if (this.hasFile(fileEntry.src) === false) {
2!
1098
                filteredFileMap.push(fileEntry);
×
1099
            }
1100
        }
1101
        const { entries, astEditor } = this.beforeProgramTranspile(fileMap, this.options.stagingDir);
4✔
1102
        const result = this._getTranspiledFileContents(
4✔
1103
            this.getFile(filePath)
1104
        );
1105
        this.afterProgramTranspile(entries, astEditor);
4✔
1106
        return result;
4✔
1107
    }
1108

1109
    /**
1110
     * Internal function used to transpile files.
1111
     * This does not write anything to the file system
1112
     */
1113
    private _getTranspiledFileContents(file: BscFile, outputPath?: string): FileTranspileResult {
1114
        const editor = new AstEditor();
253✔
1115

1116
        this.plugins.emit('beforeFileTranspile', {
253✔
1117
            program: this,
1118
            file: file,
1119
            outputPath: outputPath,
1120
            editor: editor
1121
        });
1122

1123
        //if we have any edits, assume the file needs to be transpiled
1124
        if (editor.hasChanges) {
253✔
1125
            //use the `editor` because it'll track the previous value for us and revert later on
1126
            editor.setProperty(file, 'needsTranspiled', true);
33✔
1127
        }
1128

1129
        //transpile the file
1130
        const result = file.transpile();
253✔
1131

1132
        //generate the typedef if enabled
1133
        let typedef: string;
1134
        if (isBrsFile(file) && this.options.emitDefinitions) {
253✔
1135
            typedef = file.getTypedef();
2✔
1136
        }
1137

1138
        const event: AfterFileTranspileEvent = {
253✔
1139
            program: this,
1140
            file: file,
1141
            outputPath: outputPath,
1142
            editor: editor,
1143
            code: result.code,
1144
            map: result.map,
1145
            typedef: typedef
1146
        };
1147
        this.plugins.emit('afterFileTranspile', event);
253✔
1148

1149
        //undo all `editor` edits that may have been applied to this file.
1150
        editor.undoAll();
253✔
1151

1152
        return {
253✔
1153
            srcPath: file.srcPath,
1154
            pkgPath: file.pkgPath,
1155
            code: event.code,
1156
            map: event.map,
1157
            typedef: event.typedef
1158
        };
1159
    }
1160

1161
    private beforeProgramTranspile(fileEntries: FileObj[], stagingDir: string) {
1162
        // map fileEntries using their path as key, to avoid excessive "find()" operations
1163
        const mappedFileEntries = fileEntries.reduce<Record<string, FileObj>>((collection, entry) => {
37✔
1164
            collection[s`${entry.src}`] = entry;
18✔
1165
            return collection;
18✔
1166
        }, {});
1167

1168
        const getOutputPath = (file: BscFile) => {
37✔
1169
            let filePathObj = mappedFileEntries[s`${file.srcPath}`];
77✔
1170
            if (!filePathObj) {
77✔
1171
                //this file has been added in-memory, from a plugin, for example
1172
                filePathObj = {
50✔
1173
                    //add an interpolated src path (since it doesn't actually exist in memory)
1174
                    src: `bsc:/${file.pkgPath}`,
1175
                    dest: file.pkgPath
1176
                };
1177
            }
1178
            //replace the file extension
1179
            let outputPath = filePathObj.dest.replace(/\.bs$/gi, '.brs');
77✔
1180
            //prepend the staging folder path
1181
            outputPath = s`${stagingDir}/${outputPath}`;
77✔
1182
            return outputPath;
77✔
1183
        };
1184

1185
        const entries = Object.values(this.files).map(file => {
37✔
1186
            return {
39✔
1187
                file: file,
1188
                outputPath: getOutputPath(file)
1189
            };
1190
            //sort the entries to make transpiling more deterministic
1191
        }).sort((a, b) => {
1192
            return a.file.srcPath < b.file.srcPath ? -1 : 1;
8✔
1193
        });
1194

1195
        const astEditor = new AstEditor();
37✔
1196

1197
        this.plugins.emit('beforeProgramTranspile', this, entries, astEditor);
37✔
1198
        return {
37✔
1199
            entries: entries,
1200
            getOutputPath: getOutputPath,
1201
            astEditor: astEditor
1202
        };
1203
    }
1204

1205
    public async transpile(fileEntries: FileObj[], stagingDir: string) {
1206
        const { entries, getOutputPath, astEditor } = this.beforeProgramTranspile(fileEntries, stagingDir);
32✔
1207

1208
        const processedFiles = new Set<string>();
32✔
1209

1210
        const transpileFile = async (srcPath: string, outputPath?: string) => {
32✔
1211
            //find the file in the program
1212
            const file = this.getFile(srcPath);
35✔
1213
            //mark this file as processed so we don't process it more than once
1214
            processedFiles.add(outputPath?.toLowerCase());
35!
1215

1216
            if (!this.options.pruneEmptyCodeFiles || !file.canBePruned) {
35✔
1217
                //skip transpiling typedef files
1218
                if (isBrsFile(file) && file.isTypedef) {
34✔
1219
                    return;
1✔
1220
                }
1221

1222
                const fileTranspileResult = this._getTranspiledFileContents(file, outputPath);
33✔
1223

1224
                //make sure the full dir path exists
1225
                await fsExtra.ensureDir(path.dirname(outputPath));
33✔
1226

1227
                if (await fsExtra.pathExists(outputPath)) {
33!
1228
                    throw new Error(`Error while transpiling "${file.srcPath}". A file already exists at "${outputPath}" and will not be overwritten.`);
×
1229
                }
1230
                const writeMapPromise = fileTranspileResult.map ? fsExtra.writeFile(`${outputPath}.map`, fileTranspileResult.map.toString()) : null;
33✔
1231
                await Promise.all([
33✔
1232
                    fsExtra.writeFile(outputPath, fileTranspileResult.code),
1233
                    writeMapPromise
1234
                ]);
1235

1236
                if (fileTranspileResult.typedef) {
33✔
1237
                    const typedefPath = outputPath.replace(/\.brs$/i, '.d.bs');
2✔
1238
                    await fsExtra.writeFile(typedefPath, fileTranspileResult.typedef);
2✔
1239
                }
1240
            }
1241
        };
1242

1243
        let promises = entries.map(async (entry) => {
32✔
1244
            return transpileFile(entry?.file?.srcPath, entry.outputPath);
33!
1245
        });
1246

1247
        //if there's no bslib file already loaded into the program, copy it to the staging directory
1248
        if (!this.getFile(bslibAliasedRokuModulesPkgPath) && !this.getFile(s`source/bslib.brs`)) {
32✔
1249
            promises.push(util.copyBslibToStaging(stagingDir, this.options.bslibDestinationDir));
31✔
1250
        }
1251
        await Promise.all(promises);
32✔
1252

1253
        //transpile any new files that plugins added since the start of this transpile process
1254
        do {
32✔
1255
            promises = [];
33✔
1256
            for (const key in this.files) {
33✔
1257
                const file = this.files[key];
38✔
1258
                //this is a new file
1259
                const outputPath = getOutputPath(file);
38✔
1260
                if (!processedFiles.has(outputPath?.toLowerCase())) {
38!
1261
                    promises.push(
2✔
1262
                        transpileFile(file?.srcPath, outputPath)
6!
1263
                    );
1264
                }
1265
            }
1266
            if (promises.length > 0) {
33✔
1267
                this.logger.info(`Transpiling ${promises.length} new files`);
1✔
1268
                await Promise.all(promises);
1✔
1269
            }
1270
        }
1271
        while (promises.length > 0);
1272
        this.afterProgramTranspile(entries, astEditor);
32✔
1273
    }
1274

1275
    private afterProgramTranspile(entries: TranspileObj[], astEditor: AstEditor) {
1276
        this.plugins.emit('afterProgramTranspile', this, entries, astEditor);
36✔
1277
        astEditor.undoAll();
36✔
1278
    }
1279

1280
    /**
1281
     * Find a list of files in the program that have a function with the given name (case INsensitive)
1282
     */
1283
    public findFilesForFunction(functionName: string) {
1284
        const files = [] as BscFile[];
7✔
1285
        const lowerFunctionName = functionName.toLowerCase();
7✔
1286
        //find every file with this function defined
1287
        for (const file of Object.values(this.files)) {
7✔
1288
            if (isBrsFile(file)) {
25✔
1289
                //TODO handle namespace-relative function calls
1290
                //if the file has a function with this name
1291
                if (file.parser.references.functionStatementLookup.get(lowerFunctionName) !== undefined) {
17✔
1292
                    files.push(file);
2✔
1293
                }
1294
            }
1295
        }
1296
        return files;
7✔
1297
    }
1298

1299
    /**
1300
     * Find a list of files in the program that have a class with the given name (case INsensitive)
1301
     */
1302
    public findFilesForClass(className: string) {
1303
        const files = [] as BscFile[];
7✔
1304
        const lowerClassName = className.toLowerCase();
7✔
1305
        //find every file with this class defined
1306
        for (const file of Object.values(this.files)) {
7✔
1307
            if (isBrsFile(file)) {
25✔
1308
                //TODO handle namespace-relative classes
1309
                //if the file has a function with this name
1310
                if (file.parser.references.classStatementLookup.get(lowerClassName) !== undefined) {
17✔
1311
                    files.push(file);
1✔
1312
                }
1313
            }
1314
        }
1315
        return files;
7✔
1316
    }
1317

1318
    public findFilesForNamespace(name: string) {
1319
        const files = [] as BscFile[];
7✔
1320
        const lowerName = name.toLowerCase();
7✔
1321
        //find every file with this class defined
1322
        for (const file of Object.values(this.files)) {
7✔
1323
            if (isBrsFile(file)) {
25✔
1324
                if (file.parser.references.namespaceStatements.find((x) => {
17✔
1325
                    const namespaceName = x.name.toLowerCase();
7✔
1326
                    return (
7✔
1327
                        //the namespace name matches exactly
1328
                        namespaceName === lowerName ||
9✔
1329
                        //the full namespace starts with the name (honoring the part boundary)
1330
                        namespaceName.startsWith(lowerName + '.')
1331
                    );
1332
                })) {
1333
                    files.push(file);
6✔
1334
                }
1335
            }
1336
        }
1337
        return files;
7✔
1338
    }
1339

1340
    public findFilesForEnum(name: string) {
1341
        const files = [] as BscFile[];
8✔
1342
        const lowerName = name.toLowerCase();
8✔
1343
        //find every file with this class defined
1344
        for (const file of Object.values(this.files)) {
8✔
1345
            if (isBrsFile(file)) {
26✔
1346
                if (file.parser.references.enumStatementLookup.get(lowerName)) {
18✔
1347
                    files.push(file);
1✔
1348
                }
1349
            }
1350
        }
1351
        return files;
8✔
1352
    }
1353

1354
    private _manifest: Map<string, string>;
1355

1356
    /**
1357
     * Modify a parsed manifest map by reading `bs_const` and injecting values from `options.manifest.bs_const`
1358
     * @param parsedManifest The manifest map to read from and modify
1359
     */
1360
    private buildBsConstsIntoParsedManifest(parsedManifest: Map<string, string>) {
1361
        // Lift the bs_consts defined in the manifest
1362
        let bsConsts = getBsConst(parsedManifest, false);
11✔
1363

1364
        // Override or delete any bs_consts defined in the bs config
1365
        for (const key in this.options?.manifest?.bs_const) {
11!
1366
            const value = this.options.manifest.bs_const[key];
3✔
1367
            if (value === null) {
3✔
1368
                bsConsts.delete(key);
1✔
1369
            } else {
1370
                bsConsts.set(key, value);
2✔
1371
            }
1372
        }
1373

1374
        // convert the new list of bs consts back into a string for the rest of the down stream systems to use
1375
        let constString = '';
11✔
1376
        for (const [key, value] of bsConsts) {
11✔
1377
            constString += `${constString !== '' ? ';' : ''}${key}=${value.toString()}`;
6✔
1378
        }
1379

1380
        // Set the updated bs_const value
1381
        parsedManifest.set('bs_const', constString);
11✔
1382
    }
1383

1384
    /**
1385
     * Try to find and load the manifest into memory
1386
     * @param manifestFileObj A pointer to a potential manifest file object found during loading
1387
     */
1388
    public loadManifest(manifestFileObj?: FileObj) {
1389
        let manifestPath = manifestFileObj
808✔
1390
            ? manifestFileObj.src
808✔
1391
            : path.join(this.options.rootDir, 'manifest');
1392

1393
        try {
808✔
1394
            // we only load this manifest once, so do it sync to improve speed downstream
1395
            const contents = fsExtra.readFileSync(manifestPath, 'utf-8');
808✔
1396
            const parsedManifest = parseManifest(contents);
11✔
1397
            this.buildBsConstsIntoParsedManifest(parsedManifest);
11✔
1398
            this._manifest = parsedManifest;
11✔
1399
        } catch (e) {
1400
            this._manifest = new Map();
797✔
1401
        }
1402
    }
1403

1404
    /**
1405
     * Get a map of the manifest information
1406
     */
1407
    public getManifest() {
1408
        if (!this._manifest) {
995✔
1409
            this.loadManifest();
803✔
1410
        }
1411
        return this._manifest;
995✔
1412
    }
1413

1414
    public dispose() {
1415
        this.plugins.emit('beforeProgramDispose', { program: this });
873✔
1416

1417
        for (let filePath in this.files) {
873✔
1418
            this.files[filePath].dispose();
961✔
1419
        }
1420
        for (let name in this.scopes) {
873✔
1421
            this.scopes[name].dispose();
1,754✔
1422
        }
1423
        this.globalScope.dispose();
873✔
1424
        this.dependencyGraph.dispose();
873✔
1425
    }
1426
}
1427

1428
export interface FileTranspileResult {
1429
    srcPath: string;
1430
    pkgPath: string;
1431
    code: string;
1432
    map: SourceMapGenerator;
1433
    typedef: string;
1434
}
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