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

rokucommunity / brighterscript / #15701

29 Apr 2026 03:03PM UTC coverage: 88.947% (-0.2%) from 89.135%
#15701

push

web-flow
added source fix all code action support (#1659)

Co-authored-by: Bronley Plumb <bronley@gmail.com>

8399 of 9942 branches covered (84.48%)

Branch coverage included in aggregate %.

32 of 63 new or added lines in 9 files covered. (50.79%)

1 existing line in 1 file now uncovered.

10673 of 11500 relevant lines covered (92.81%)

2034.52 hits per line

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

93.37
/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, DocumentSymbol, CancellationToken, SelectionRange } from 'vscode-languageserver';
5
import { CancellationTokenSource, CompletionItemKind } from 'vscode-languageserver';
1✔
6
import type { BsConfig, FinalizedBsConfig } from './BsConfig';
7
import { Scope } from './Scope';
1✔
8
import type { NamespaceContainer, NamespaceFileContribution } from './Scope';
9
import { SymbolTable } from './SymbolTable';
1✔
10
import { DiagnosticMessages } from './DiagnosticMessages';
1✔
11
import { BrsFile } from './files/BrsFile';
1✔
12
import { XmlFile } from './files/XmlFile';
1✔
13
import type { BsDiagnostic, File, FileReference, FileObj, BscFile, SemanticToken, AfterFileTranspileEvent, FileLink, ProvideHoverEvent, ProvideCompletionsEvent, Hover, ProvideDefinitionEvent, ProvideReferencesEvent, ProvideDocumentSymbolsEvent, ProvideWorkspaceSymbolsEvent, ProvideSelectionRangesEvent, OnGetSourceFixAllCodeActionsEvent } from './interfaces';
14
import type { SourceFixAllCodeAction } from './CodeActionUtil';
15
import { codeActionUtil } from './CodeActionUtil';
1✔
16
import { standardizePath as s, util } from './util';
1✔
17
import { XmlScope } from './XmlScope';
1✔
18
import { DiagnosticFilterer } from './DiagnosticFilterer';
1✔
19
import { DependencyGraph } from './DependencyGraph';
1✔
20
import type { Logger } from './logging';
21
import { LogLevel, createLogger } from './logging';
1✔
22
import chalk from 'chalk';
1✔
23
import { globalFile } from './globalCallables';
1✔
24
import { parseManifest, getBsConst } from './preprocessor/Manifest';
1✔
25
import { URI } from 'vscode-uri';
1✔
26
import PluginInterface from './PluginInterface';
1✔
27
import { isBrsFile, isXmlFile, isXmlScope, isNamespaceStatement } from './astUtils/reflection';
1✔
28
import type { FunctionStatement, NamespaceStatement } from './parser/Statement';
29
import { BscPlugin } from './bscPlugin/BscPlugin';
1✔
30
import { AstEditor } from './astUtils/AstEditor';
1✔
31
import type { SourceMapGenerator } from 'source-map';
32
import type { Statement } from './parser/AstNode';
33
import { CallExpressionInfo } from './bscPlugin/CallExpressionInfo';
1✔
34
import { SignatureHelpUtil } from './bscPlugin/SignatureHelpUtil';
1✔
35
import { DiagnosticSeverityAdjuster } from './DiagnosticSeverityAdjuster';
1✔
36
import { Sequencer } from './common/Sequencer';
1✔
37
import { Deferred } from './deferred';
1✔
38

39
const startOfSourcePkgPath = `source${path.sep}`;
1✔
40
const bslibNonAliasedRokuModulesPkgPath = s`source/roku_modules/rokucommunity_bslib/bslib.brs`;
1✔
41
const bslibAliasedRokuModulesPkgPath = s`source/roku_modules/bslib/bslib.brs`;
1✔
42

43
export interface SourceObj {
44
    /**
45
     * @deprecated use `srcPath` instead
46
     */
47
    pathAbsolute: string;
48
    srcPath: string;
49
    source: string;
50
    definitions?: string;
51
}
52

53
export interface TranspileObj {
54
    file: BscFile;
55
    outputPath: string;
56
}
57

58
export interface SignatureInfoObj {
59
    index: number;
60
    key: string;
61
    signature: SignatureInformation;
62
}
63

64
export class Program {
1✔
65
    constructor(
66
        /**
67
         * The root directory for this program
68
         */
69
        options: BsConfig,
70
        logger?: Logger,
71
        plugins?: PluginInterface
72
    ) {
73
        this.options = util.normalizeConfig(options);
1,576✔
74
        this.logger = logger ?? createLogger(options);
1,576✔
75
        this.plugins = plugins || new PluginInterface([], { logger: this.logger });
1,576✔
76

77
        //inject the bsc plugin as the first plugin in the stack.
78
        this.plugins.addFirst(new BscPlugin());
1,576✔
79

80
        //normalize the root dir path
81
        this.options.rootDir = util.getRootDir(this.options);
1,576✔
82

83
        this.createGlobalScope();
1,576✔
84
    }
85

86
    public options: FinalizedBsConfig;
87
    public logger: Logger;
88

89
    private createGlobalScope() {
90
        //create the 'global' scope
91
        this.globalScope = new Scope('global', this, 'scope:global');
1,576✔
92
        this.globalScope.attachDependencyGraph(this.dependencyGraph);
1,576✔
93
        this.scopes.global = this.globalScope;
1,576✔
94
        //hardcode the files list for global scope to only contain the global file
95
        this.globalScope.getAllFiles = () => [globalFile];
25,552✔
96
        this.globalScope.validate();
1,576✔
97
        //for now, disable validation of global scope because the global files have some duplicate method declarations
98
        this.globalScope.getDiagnostics = () => [];
1,576✔
99
        //TODO we might need to fix this because the isValidated clears stuff now
100
        (this.globalScope as any).isValidated = true;
1,576✔
101
    }
102

103
    /**
104
     * A graph of all files and their dependencies.
105
     * For example:
106
     *      File.xml -> [lib1.brs, lib2.brs]
107
     *      lib2.brs -> [lib3.brs] //via an import statement
108
     */
109
    private dependencyGraph = new DependencyGraph();
1,576✔
110

111
    private diagnosticFilterer = new DiagnosticFilterer();
1,576✔
112

113
    private diagnosticAdjuster = new DiagnosticSeverityAdjuster();
1,576✔
114

115
    /**
116
     * A scope that contains all built-in global functions.
117
     * All scopes should directly or indirectly inherit from this scope
118
     */
119
    public globalScope: Scope = undefined as any;
1,576✔
120

121
    /**
122
     * Plugins which can provide extra diagnostics or transform AST
123
     */
124
    public plugins: PluginInterface;
125

126
    /**
127
     * A set of diagnostics. This does not include any of the scope diagnostics.
128
     * Should only be set from `this.validate()`
129
     */
130
    private diagnostics = [] as BsDiagnostic[];
1,576✔
131

132
    /**
133
     * The path to bslib.brs (the BrightScript runtime for certain BrighterScript features)
134
     */
135
    public get bslibPkgPath() {
136
        //if there's an aliased (preferred) version of bslib from roku_modules loaded into the program, use that
137
        if (this.getFile(bslibAliasedRokuModulesPkgPath)) {
472✔
138
            return bslibAliasedRokuModulesPkgPath;
2✔
139

140
            //if there's a non-aliased version of bslib from roku_modules, use that
141
        } else if (this.getFile(bslibNonAliasedRokuModulesPkgPath)) {
470✔
142
            return bslibNonAliasedRokuModulesPkgPath;
3✔
143

144
            //default to the embedded version
145
        } else {
146
            return `${this.options.bslibDestinationDir}${path.sep}bslib.brs`;
467✔
147
        }
148
    }
149

150
    public get bslibPrefix() {
151
        if (this.bslibPkgPath === bslibNonAliasedRokuModulesPkgPath) {
441✔
152
            return 'rokucommunity_bslib';
3✔
153
        } else {
154
            return 'bslib';
438✔
155
        }
156
    }
157

158

159
    /**
160
     * A map of every file loaded into this program, indexed by its original file location
161
     */
162
    public files = {} as Record<string, BscFile>;
1,576✔
163
    private pkgMap = {} as Record<string, BscFile>;
1,576✔
164

165
    /**
166
     * Map from a lower-cased namespace name part to the set of `BrsFile`s that contribute
167
     * to it. Built lazily, invalidated whenever any file is added, removed, or re-parsed
168
     * (`setFile` and `removeFile` both clear it).
169
     *
170
     * Used by `ScopeNamespaceLookup` to resolve a namespace name to its contributing
171
     * files in O(1), then intersect against the scope's file set.
172
     */
173
    private namespaceContributors: Map<string, Set<BrsFile>> | undefined;
174

175
    /**
176
     * Look up the set of `BrsFile`s that declare any part of the given namespace name
177
     * (lowercased). Returns `undefined` when no file contributes.
178
     * @internal
179
     */
180
    protected getNamespaceContributors(namespaceNameLower: string): Set<BrsFile> | undefined {
181
        if (!this.namespaceContributors) {
332✔
182
            this.namespaceContributors = this.buildNamespaceContributors();
200✔
183
        }
184
        return this.namespaceContributors.get(namespaceNameLower);
332✔
185
    }
186

187
    private buildNamespaceContributors(): Map<string, Set<BrsFile>> {
188
        const contributors = new Map<string, Set<BrsFile>>();
200✔
189
        for (const file of Object.values(this.files)) {
200✔
190
            if (isBrsFile(file)) {
274✔
191
                // eslint-disable-next-line @typescript-eslint/dot-notation
192
                for (const nameLower of file['getNamespaceContributions']().keys()) {
256✔
193
                    let set = contributors.get(nameLower);
357✔
194
                    if (!set) {
357✔
195
                        set = new Set<BrsFile>();
329✔
196
                        contributors.set(nameLower, set);
329✔
197
                    }
198
                    set.add(file);
357✔
199
                }
200
            }
201
        }
202
        return contributors;
200✔
203
    }
204

205
    /**
206
     * Cached slow-path namespace aggregates, keyed by `(nameLower, sorted-contributor-pkgPaths)`.
207
     * Two scopes with the same in-scope file set for a multi-contributor namespace share
208
     * the same aggregate object (and therefore the same merged statement collections and
209
     * symbolTable instance). Built lazily, invalidated alongside `namespaceContributors`.
210
     *
211
     * The aggregate is stored as a `NamespaceContainer` whose `namespaces` field is an
212
     * empty Map: scopes always wrap the aggregate before returning to plugins, and the
213
     * wrapper supplies its own scope-filtered children. Plugins never see the aggregate
214
     * directly.
215
     */
216
    private aggregateNamespaceContainerCache: Map<string, NamespaceContainer> | undefined;
217

218
    /**
219
     * Get or build the shared aggregate for a namespace whose in-scope contributors
220
     * include more than one file. The aggregate's heavy fields are computed once per
221
     * unique `(nameLower, contributing-files-set)` and reused across every scope that
222
     * sees the same set.
223
     * @internal
224
     */
225
    protected getAggregateNamespaceContainer(nameLower: string, contributions: NamespaceFileContribution[]): NamespaceContainer {
226
        if (!this.aggregateNamespaceContainerCache) {
33✔
227
            this.aggregateNamespaceContainerCache = new Map<string, NamespaceContainer>();
22✔
228
        }
229
        //sorted pkgPaths ensure two scopes with the same contributor set hit the same key
230
        const key = nameLower + '|' + contributions
33✔
231
            .map(c => c.file.pkgPath.toLowerCase())
69✔
232
            .sort()
233
            .join('|');
234
        let aggregate = this.aggregateNamespaceContainerCache.get(key);
33✔
235
        if (!aggregate) {
33✔
236
            aggregate = this.buildAggregateNamespaceContainer(contributions);
26✔
237
            this.aggregateNamespaceContainerCache.set(key, aggregate);
26✔
238
        }
239
        return aggregate;
33✔
240
    }
241

242
    private buildAggregateNamespaceContainer(contributions: NamespaceFileContribution[]): NamespaceContainer {
243
        const first = contributions[0];
26✔
244
        //field order matches the NamespaceContainer interface declaration so aggregates
245
        //share a single V8 hidden class with the per-scope wrapper containers
246
        const aggregate: NamespaceContainer = {
26✔
247
            file: first.file,
248
            fullName: first.fullName,
249
            nameRange: first.nameRange,
250
            lastPartName: first.lastPartName,
251
            namespaces: new Map(),
252
            statements: undefined,
253
            classStatements: undefined,
254
            functionStatements: undefined,
255
            enumStatements: undefined,
256
            constStatements: undefined,
257
            symbolTable: undefined
258
        };
259
        for (const contribution of contributions) {
26✔
260
            if (contribution.statements?.length) {
55✔
261
                (aggregate.statements ??= []).push(...contribution.statements);
45✔
262
            }
263
            if (contribution.classStatements) {
55✔
264
                aggregate.classStatements = { ...(aggregate.classStatements ?? {}), ...contribution.classStatements };
7✔
265
            }
266
            if (contribution.functionStatements) {
55✔
267
                aggregate.functionStatements = { ...(aggregate.functionStatements ?? {}), ...contribution.functionStatements };
29✔
268
            }
269
            if (contribution.enumStatements) {
55✔
270
                aggregate.enumStatements ??= new Map();
5!
271
                for (const [key, value] of contribution.enumStatements) {
5✔
272
                    aggregate.enumStatements.set(key, value);
5✔
273
                }
274
            }
275
            if (contribution.constStatements) {
55✔
276
                aggregate.constStatements ??= new Map();
6✔
277
                for (const [key, value] of contribution.constStatements) {
6✔
278
                    aggregate.constStatements.set(key, value);
7✔
279
                }
280
            }
281
            if (contribution.symbolTable) {
55✔
282
                aggregate.symbolTable ??= new SymbolTable(`Namespace Multi-File Aggregate: '${first.fullName}'`);
45✔
283
                aggregate.symbolTable.mergeSymbolTable(contribution.symbolTable);
45✔
284
            }
285
        }
286
        return aggregate;
26✔
287
    }
288

289
    /**
290
     * Invalidate the program-level namespace contributors map and the slow-path aggregate
291
     * cache. Called by `setFile` and `removeFile`; downstream scope namespace lookups
292
     * already rebuild via the dependency-graph invalidation chain, so this only needs
293
     * to drop the cached maps.
294
     */
295
    private invalidateNamespaceContributorCache() {
296
        this.namespaceContributors = undefined;
1,798✔
297
        this.aggregateNamespaceContainerCache = undefined;
1,798✔
298
    }
299

300
    private scopes = {} as Record<string, Scope>;
1,576✔
301

302
    protected addScope(scope: Scope) {
303
        this.scopes[scope.name] = scope;
1,455✔
304
    }
305

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

314
    /**
315
     * Get the component with the specified name
316
     */
317
    public getComponent(componentName: string) {
318
        if (componentName) {
668✔
319
            //return the first compoment in the list with this name
320
            //(components are ordered in this list by pkgPath to ensure consistency)
321
            return this.components[componentName.toLowerCase()]?.[0];
625✔
322
        } else {
323
            return undefined;
43✔
324
        }
325
    }
326

327
    /**
328
     * Register (or replace) the reference to a component in the component map
329
     */
330
    private registerComponent(xmlFile: XmlFile, scope: XmlScope) {
331
        const key = (xmlFile.componentName?.text ?? xmlFile.pkgPath).toLowerCase();
269✔
332
        if (!this.components[key]) {
269✔
333
            this.components[key] = [];
257✔
334
        }
335
        this.components[key].push({
269✔
336
            file: xmlFile,
337
            scope: scope
338
        });
339
        this.components[key].sort((a, b) => {
269✔
340
            const pathA = a.file.pkgPath.toLowerCase();
5✔
341
            const pathB = b.file.pkgPath.toLowerCase();
5✔
342
            if (pathA < pathB) {
5✔
343
                return -1;
1✔
344
            } else if (pathA > pathB) {
4!
345
                return 1;
4✔
346
            }
347
            return 0;
×
348
        });
349
        this.syncComponentDependencyGraph(this.components[key]);
269✔
350
    }
351

352
    /**
353
     * Remove the specified component from the components map
354
     */
355
    private unregisterComponent(xmlFile: XmlFile) {
356
        const key = (xmlFile.componentName?.text ?? xmlFile.pkgPath).toLowerCase();
11✔
357
        const arr = this.components[key] || [];
11!
358
        for (let i = 0; i < arr.length; i++) {
11✔
359
            if (arr[i].file === xmlFile) {
11!
360
                arr.splice(i, 1);
11✔
361
                break;
11✔
362
            }
363
        }
364
        this.syncComponentDependencyGraph(arr);
11✔
365
    }
366

367
    /**
368
     * re-attach the dependency graph with a new key for any component who changed
369
     * their position in their own named array (only matters when there are multiple
370
     * components with the same name)
371
     */
372
    private syncComponentDependencyGraph(components: Array<{ file: XmlFile; scope: XmlScope }>) {
373
        //reattach every dependency graph
374
        for (let i = 0; i < components.length; i++) {
280✔
375
            const { file, scope } = components[i];
275✔
376

377
            //attach (or re-attach) the dependencyGraph for every component whose position changed
378
            if (file.dependencyGraphIndex !== i) {
275✔
379
                file.dependencyGraphIndex = i;
271✔
380
                file.attachDependencyGraph(this.dependencyGraph);
271✔
381
                scope.attachDependencyGraph(this.dependencyGraph);
271✔
382
            }
383
        }
384
    }
385

386
    /**
387
     * Get a list of all files that are included in the project but are not referenced
388
     * by any scope in the program.
389
     */
390
    public getUnreferencedFiles() {
391
        let result = [] as File[];
1,030✔
392
        for (let filePath in this.files) {
1,030✔
393
            let file = this.files[filePath];
1,269✔
394
            //is this file part of a scope
395
            if (!this.getFirstScopeForFile(file)) {
1,269✔
396
                //no scopes reference this file. add it to the list
397
                result.push(file);
45✔
398
            }
399
        }
400
        return result;
1,030✔
401
    }
402

403
    /**
404
     * Get the list of errors for the entire program. It's calculated on the fly
405
     * by walking through every file, so call this sparingly.
406
     */
407
    public getDiagnostics() {
408
        return this.logger.time(LogLevel.info, ['Program.getDiagnostics()'], () => {
1,030✔
409

410
            let diagnostics = [...this.diagnostics];
1,030✔
411

412
            //get the diagnostics from all scopes
413
            for (let scopeName in this.scopes) {
1,030✔
414
                let scope = this.scopes[scopeName];
2,090✔
415
                diagnostics.push(
2,090✔
416
                    ...scope.getDiagnostics()
417
                );
418
            }
419

420
            //get the diagnostics from all unreferenced files
421
            let unreferencedFiles = this.getUnreferencedFiles();
1,030✔
422
            for (let file of unreferencedFiles) {
1,030✔
423
                diagnostics.push(
45✔
424
                    ...file.getDiagnostics()
425
                );
426
            }
427
            const filteredDiagnostics = this.logger.time(LogLevel.debug, ['filter diagnostics'], () => {
1,030✔
428
                //filter out diagnostics based on our diagnostic filters
429
                let finalDiagnostics = this.diagnosticFilterer.filter({
1,030✔
430
                    ...this.options,
431
                    rootDir: this.options.rootDir
432
                }, diagnostics);
433
                return finalDiagnostics;
1,030✔
434
            });
435

436
            this.logger.time(LogLevel.debug, ['adjust diagnostics severity'], () => {
1,030✔
437
                this.diagnosticAdjuster.adjust(this.options, diagnostics);
1,030✔
438
            });
439

440
            this.logger.info(`diagnostic counts: total=${chalk.yellow(diagnostics.length.toString())}, after filter=${chalk.yellow(filteredDiagnostics.length.toString())}`);
1,030✔
441
            return filteredDiagnostics;
1,030✔
442
        });
443
    }
444

445
    public addDiagnostics(diagnostics: BsDiagnostic[]) {
446
        this.diagnostics.push(...diagnostics);
45✔
447
    }
448

449
    /**
450
     * Determine if the specified file is loaded in this program right now.
451
     * @param filePath the absolute or relative path to the file
452
     * @param normalizePath should the provided path be normalized before use
453
     */
454
    public hasFile(filePath: string, normalizePath = true) {
1,678✔
455
        return !!this.getFile(filePath, normalizePath);
1,678✔
456
    }
457

458
    public getPkgPath(...args: any[]): any { //eslint-disable-line
459
        throw new Error('Not implemented');
×
460
    }
461

462
    /**
463
     * roku filesystem is case INsensitive, so find the scope by key case insensitive
464
     */
465
    public getScopeByName(scopeName: string): Scope | undefined {
466
        if (!scopeName) {
35!
467
            return undefined;
×
468
        }
469
        //most scopes are xml file pkg paths. however, the ones that are not are single names like "global" and "scope",
470
        //so it's safe to run the standardizePkgPath method
471
        scopeName = s`${scopeName}`;
35✔
472
        let key = Object.keys(this.scopes).find(x => x.toLowerCase() === scopeName.toLowerCase());
86✔
473
        return this.scopes[key!];
35✔
474
    }
475

476
    /**
477
     * Return all scopes
478
     */
479
    public getScopes() {
480
        return Object.values(this.scopes);
9✔
481
    }
482

483
    /**
484
     * Find the scope for the specified component
485
     */
486
    public getComponentScope(componentName: string) {
487
        return this.getComponent(componentName)?.scope;
197✔
488
    }
489

490
    /**
491
     * Update internal maps with this file reference
492
     */
493
    private assignFile<T extends BscFile = BscFile>(file: T) {
494
        this.files[file.srcPath.toLowerCase()] = file;
1,638✔
495
        this.pkgMap[file.pkgPath.toLowerCase()] = file;
1,638✔
496
        return file;
1,638✔
497
    }
498

499
    /**
500
     * Remove this file from internal maps
501
     */
502
    private unassignFile<T extends BscFile = BscFile>(file: T) {
503
        delete this.files[file.srcPath.toLowerCase()];
156✔
504
        delete this.pkgMap[file.pkgPath.toLowerCase()];
156✔
505
        return file;
156✔
506
    }
507

508
    /**
509
     * Load a file into the program. If that file already exists, it is replaced.
510
     * If file contents are provided, those are used, Otherwise, the file is loaded from the file system
511
     * @param srcPath the file path relative to the root dir
512
     * @param fileContents the file contents
513
     * @deprecated use `setFile` instead
514
     */
515
    public addOrReplaceFile<T extends BscFile>(srcPath: string, fileContents: string): T;
516
    /**
517
     * Load a file into the program. If that file already exists, it is replaced.
518
     * @param fileEntry an object that specifies src and dest for the file.
519
     * @param fileContents the file contents. If not provided, the file will be loaded from disk
520
     * @deprecated use `setFile` instead
521
     */
522
    public addOrReplaceFile<T extends BscFile>(fileEntry: FileObj, fileContents: string): T;
523
    public addOrReplaceFile<T extends BscFile>(fileParam: FileObj | string, fileContents: string): T {
524
        return this.setFile<T>(fileParam as any, fileContents);
1✔
525
    }
526

527
    /**
528
     * Load a file into the program. If that file already exists, it is replaced.
529
     * If file contents are provided, those are used, Otherwise, the file is loaded from the file system
530
     * @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:/`)
531
     * @param fileContents the file contents
532
     */
533
    public setFile<T extends BscFile>(srcDestOrPkgPath: string, fileContents: string): T;
534
    /**
535
     * Load a file into the program. If that file already exists, it is replaced.
536
     * @param fileEntry an object that specifies src and dest for the file.
537
     * @param fileContents the file contents. If not provided, the file will be loaded from disk
538
     */
539
    public setFile<T extends BscFile>(fileEntry: FileObj, fileContents: string): T;
540
    public setFile<T extends BscFile>(fileParam: FileObj | string, fileContents: string): T {
541
        //normalize the file paths
542
        const { srcPath, pkgPath } = this.getPaths(fileParam, this.options.rootDir);
1,642✔
543

544
        //namespace contributions for the new/replaced file may differ; force the
545
        //program-level contributors map to rebuild on next query
546
        this.invalidateNamespaceContributorCache();
1,642✔
547

548
        let file = this.logger.time(LogLevel.debug, ['Program.setFile()', chalk.green(srcPath)], () => {
1,642✔
549
            //if the file is already loaded, remove it
550
            if (this.hasFile(srcPath)) {
1,642✔
551
                this.removeFile(srcPath);
139✔
552
            }
553
            let fileExtension = path.extname(srcPath).toLowerCase();
1,642✔
554
            let file: BscFile | undefined;
555

556
            if (fileExtension === '.brs' || fileExtension === '.bs') {
1,642✔
557
                //add the file to the program
558
                const brsFile = this.assignFile(
1,369✔
559
                    new BrsFile(srcPath, pkgPath, this)
560
                );
561

562
                //add file to the `source` dependency list
563
                if (brsFile.pkgPath.startsWith(startOfSourcePkgPath)) {
1,369✔
564
                    this.createSourceScope();
1,181✔
565
                    this.dependencyGraph.addDependency('scope:source', brsFile.dependencyGraphKey);
1,181✔
566
                }
567

568
                let sourceObj: SourceObj = {
1,369✔
569
                    //TODO remove `pathAbsolute` in v1
570
                    pathAbsolute: srcPath,
571
                    srcPath: srcPath,
572
                    source: fileContents
573
                };
574
                this.plugins.emit('beforeFileParse', sourceObj);
1,369✔
575

576
                this.logger.time(LogLevel.debug, ['parse', chalk.green(srcPath)], () => {
1,369✔
577
                    brsFile.parse(sourceObj.source);
1,369✔
578
                });
579

580
                //notify plugins that this file has finished parsing
581
                this.plugins.emit('afterFileParse', brsFile);
1,369✔
582

583
                file = brsFile;
1,369✔
584

585
                brsFile.attachDependencyGraph(this.dependencyGraph);
1,369✔
586

587
            } else if (
273✔
588
                //is xml file
589
                fileExtension === '.xml' &&
545✔
590
                //resides in the components folder (Roku will only parse xml files in the components folder)
591
                pkgPath.toLowerCase().startsWith(util.pathSepNormalize(`components/`))
592
            ) {
593
                //add the file to the program
594
                const xmlFile = this.assignFile(
269✔
595
                    new XmlFile(srcPath, pkgPath, this)
596
                );
597

598
                let sourceObj: SourceObj = {
269✔
599
                    //TODO remove `pathAbsolute` in v1
600
                    pathAbsolute: srcPath,
601
                    srcPath: srcPath,
602
                    source: fileContents
603
                };
604
                this.plugins.emit('beforeFileParse', sourceObj);
269✔
605

606
                this.logger.time(LogLevel.debug, ['parse', chalk.green(srcPath)], () => {
269✔
607
                    xmlFile.parse(sourceObj.source);
269✔
608
                });
609

610
                //notify plugins that this file has finished parsing
611
                this.plugins.emit('afterFileParse', xmlFile);
269✔
612

613
                file = xmlFile;
269✔
614

615
                //create a new scope for this xml file
616
                let scope = new XmlScope(xmlFile, this);
269✔
617
                this.addScope(scope);
269✔
618

619
                //register this compoent now that we have parsed it and know its component name
620
                this.registerComponent(xmlFile, scope);
269✔
621

622
                //notify plugins that the scope is created and the component is registered
623
                this.plugins.emit('afterScopeCreate', scope);
269✔
624
            } else {
625
                //TODO do we actually need to implement this? Figure out how to handle img paths
626
                // let genericFile = this.files[srcPath] = <any>{
627
                //     srcPath: srcPath,
628
                //     pkgPath: pkgPath,
629
                //     wasProcessed: true
630
                // } as File;
631
                // file = <any>genericFile;
632
            }
633
            return file;
1,642✔
634
        });
635
        return file as T;
1,642✔
636
    }
637

638
    /**
639
     * Given a srcPath, a pkgPath, or both, resolve whichever is missing, relative to rootDir.
640
     * @param fileParam an object representing file paths
641
     * @param rootDir must be a pre-normalized path
642
     */
643
    private getPaths(fileParam: string | FileObj | { srcPath?: string; pkgPath?: string }, rootDir: string) {
644
        let srcPath: string | undefined;
645
        let pkgPath: string | undefined;
646

647
        assert.ok(fileParam, 'fileParam is required');
1,649✔
648

649
        //lift the srcPath and pkgPath vars from the incoming param
650
        if (typeof fileParam === 'string') {
1,649✔
651
            fileParam = this.removePkgPrefix(fileParam);
1,180✔
652
            srcPath = s`${path.resolve(rootDir, fileParam)}`;
1,180✔
653
            pkgPath = s`${util.replaceCaseInsensitive(srcPath, rootDir, '')}`;
1,180✔
654
        } else {
655
            let param: any = fileParam;
469✔
656

657
            if (param.src) {
469✔
658
                srcPath = s`${param.src}`;
466✔
659
            }
660
            if (param.srcPath) {
469✔
661
                srcPath = s`${param.srcPath}`;
2✔
662
            }
663
            if (param.dest) {
469✔
664
                pkgPath = s`${this.removePkgPrefix(param.dest)}`;
466✔
665
            }
666
            if (param.pkgPath) {
469✔
667
                pkgPath = s`${this.removePkgPrefix(param.pkgPath)}`;
2✔
668
            }
669
        }
670

671
        //if there's no srcPath, use the pkgPath to build an absolute srcPath
672
        if (!srcPath) {
1,649✔
673
            srcPath = s`${rootDir}/${pkgPath}`;
1✔
674
        }
675
        //coerce srcPath to an absolute path
676
        if (!path.isAbsolute(srcPath)) {
1,649✔
677
            srcPath = util.standardizePath(srcPath);
1✔
678
        }
679

680
        //if there's no pkgPath, compute relative path from rootDir
681
        if (!pkgPath) {
1,649✔
682
            pkgPath = s`${util.replaceCaseInsensitive(srcPath, rootDir, '')}`;
1✔
683
        }
684

685
        assert.ok(srcPath, 'fileEntry.src is required');
1,649✔
686
        assert.ok(pkgPath, 'fileEntry.dest is required');
1,649✔
687

688
        return {
1,649✔
689
            srcPath: srcPath,
690
            //remove leading slash from pkgPath
691
            pkgPath: pkgPath.replace(/^[\/\\]+/, '')
692
        };
693
    }
694

695
    /**
696
     * Remove any leading `pkg:/` found in the path
697
     */
698
    private removePkgPrefix(path: string) {
699
        return path.replace(/^pkg:\//i, '');
1,648✔
700
    }
701

702
    /**
703
     * Ensure source scope is created.
704
     * Note: automatically called internally, and no-op if it exists already.
705
     */
706
    public createSourceScope() {
707
        if (!this.scopes.source) {
1,633✔
708
            const sourceScope = new Scope('source', this, 'scope:source');
1,186✔
709
            sourceScope.attachDependencyGraph(this.dependencyGraph);
1,186✔
710
            this.addScope(sourceScope);
1,186✔
711
            this.plugins.emit('afterScopeCreate', sourceScope);
1,186✔
712
        }
713
    }
714

715
    /**
716
     * Find the file by its absolute path. This is case INSENSITIVE, since
717
     * Roku is a case insensitive file system. It is an error to have multiple files
718
     * with the same path with only case being different.
719
     * @param srcPath the absolute path to the file
720
     * @deprecated use `getFile` instead, which auto-detects the path type
721
     */
722
    public getFileByPathAbsolute<T extends BrsFile | XmlFile>(srcPath: string) {
723
        srcPath = s`${srcPath}`;
×
724
        for (let filePath in this.files) {
×
725
            if (filePath.toLowerCase() === srcPath.toLowerCase()) {
×
726
                return this.files[filePath] as T;
×
727
            }
728
        }
729
    }
730

731
    /**
732
     * Get a list of files for the given (platform-normalized) pkgPath array.
733
     * Missing files are just ignored.
734
     * @deprecated use `getFiles` instead, which auto-detects the path types
735
     */
736
    public getFilesByPkgPaths<T extends BscFile[]>(pkgPaths: string[]) {
737
        return pkgPaths
×
738
            .map(pkgPath => this.getFileByPkgPath(pkgPath))
×
739
            .filter(file => file !== undefined) as T;
×
740
    }
741

742
    /**
743
     * Get a file with the specified (platform-normalized) pkg path.
744
     * If not found, return undefined
745
     * @deprecated use `getFile` instead, which auto-detects the path type
746
     */
747
    public getFileByPkgPath<T extends BscFile>(pkgPath: string) {
748
        return this.pkgMap[pkgPath.toLowerCase()] as T;
486✔
749
    }
750

751
    /**
752
     * Remove a set of files from the program
753
     * @param srcPaths can be an array of srcPath or destPath strings
754
     * @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
755
     */
756
    public removeFiles(srcPaths: string[], normalizePath = true) {
1✔
757
        for (let srcPath of srcPaths) {
1✔
758
            this.removeFile(srcPath, normalizePath);
1✔
759
        }
760
    }
761

762
    /**
763
     * Remove a file from the program
764
     * @param filePath can be a srcPath, a pkgPath, or a destPath (same as pkgPath but without `pkg:/`)
765
     * @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
766
     */
767
    public removeFile(filePath: string, normalizePath = true) {
152✔
768
        this.logger.debug('Program.removeFile()', filePath);
156✔
769

770
        //namespace contributions may have included this file; force the program-level
771
        //contributors map to rebuild on next query
772
        this.invalidateNamespaceContributorCache();
156✔
773

774
        let file = this.getFile(filePath, normalizePath);
156✔
775
        if (file) {
156!
776
            this.plugins.emit('beforeFileDispose', file);
156✔
777

778
            //if there is a scope named the same as this file's path, remove it (i.e. xml scopes)
779
            let scope = this.scopes[file.pkgPath];
156✔
780
            if (scope) {
156✔
781
                this.plugins.emit('beforeScopeDispose', scope);
11✔
782
                scope.dispose();
11✔
783
                //notify dependencies of this scope that it has been removed
784
                this.dependencyGraph.remove(scope.dependencyGraphKey!);
11✔
785
                delete this.scopes[file.pkgPath];
11✔
786
                this.plugins.emit('afterScopeDispose', scope);
11✔
787
            }
788
            //remove the file from the program
789
            this.unassignFile(file);
156✔
790

791
            this.dependencyGraph.remove(file.dependencyGraphKey);
156✔
792

793
            //if this is a pkg:/source file, notify the `source` scope that it has changed
794
            if (file.pkgPath.startsWith(startOfSourcePkgPath)) {
156✔
795
                this.dependencyGraph.removeDependency('scope:source', file.dependencyGraphKey);
105✔
796
            }
797

798
            //if this is a component, remove it from our components map
799
            if (isXmlFile(file)) {
156✔
800
                this.unregisterComponent(file);
11✔
801
            }
802
            //dispose file
803
            file?.dispose();
156!
804
            this.plugins.emit('afterFileDispose', file);
156✔
805
        }
806
    }
807

808
    /**
809
     * Counter used to track which validation run is being logged
810
     */
811
    private validationRunSequence = 1;
1,576✔
812

813
    /**
814
     * How many milliseconds can pass while doing synchronous operations in validate before we register a short timeout (i.e. yield to the event loop)
815
     */
816
    private validationMinSyncDuration = 75;
1,576✔
817

818
    private validatePromise: Promise<void> | undefined;
819

820
    /**
821
     * Traverse the entire project, and validate all scopes
822
     */
823
    public validate(): void;
824
    public validate(options: { async: false; cancellationToken?: CancellationToken }): void;
825
    public validate(options: { async: true; cancellationToken?: CancellationToken }): Promise<void>;
826
    public validate(options?: { async?: boolean; cancellationToken?: CancellationToken }) {
827
        const validationRunId = this.validationRunSequence++;
986✔
828
        const timeEnd = this.logger.timeStart(LogLevel.log, `Validating project${(this.logger.logLevel as LogLevel) > LogLevel.log ? ` (run ${validationRunId})` : ''}`);
986!
829

830
        let previousValidationPromise = this.validatePromise;
986✔
831
        const deferred = new Deferred();
986✔
832

833
        if (options?.async) {
986✔
834
            //we're async, so create a new promise chain to resolve after this validation is done
835
            this.validatePromise = Promise.resolve(previousValidationPromise).then(() => {
158✔
836
                return deferred.promise;
158✔
837
            });
838

839
            //we are not async but there's a pending promise, then we cannot run this validation
840
        } else if (previousValidationPromise !== undefined) {
828!
841
            throw new Error('Cannot run synchronous validation while an async validation is in progress');
×
842
        }
843

844
        if (options?.async) {
986✔
845
            //we're async, so create a new promise chain to resolve after this validation is done
846
            this.validatePromise = Promise.resolve(previousValidationPromise).then(() => {
158✔
847
                return deferred.promise;
158✔
848
            });
849

850
            //we are not async but there's a pending promise, then we cannot run this validation
851
        } else if (previousValidationPromise !== undefined) {
828!
852
            throw new Error('Cannot run synchronous validation while an async validation is in progress');
×
853
        }
854

855
        const sequencer = new Sequencer({
986✔
856
            name: 'program.validate',
857
            cancellationToken: options?.cancellationToken ?? new CancellationTokenSource().token,
5,916✔
858
            minSyncDuration: this.validationMinSyncDuration
859
        });
860

861
        let beforeProgramValidateWasEmitted = false;
986✔
862

863
        //this sequencer allows us to run in both sync and async mode, depending on whether options.async is enabled.
864
        //We use this to prevent starving the CPU during long validate cycles when running in a language server context
865
        sequencer
986✔
866
            .once(() => {
867
                //if running in async mode, return the previous validation promise to ensure we're only running one at a time
868
                if (options?.async) {
986✔
869
                    return previousValidationPromise;
158✔
870
                }
871
            })
872
            .once(() => {
873
                this.diagnostics = [];
983✔
874
                this.plugins.emit('beforeProgramValidate', this);
983✔
875
                beforeProgramValidateWasEmitted = true;
983✔
876
            })
877
            .forEach(() => Object.values(this.files), (file) => {
983✔
878
                if (!file.isValidated) {
1,269✔
879
                    this.plugins.emit('beforeFileValidate', {
1,206✔
880
                        program: this,
881
                        file: file
882
                    });
883

884
                    //emit an event to allow plugins to contribute to the file validation process
885
                    this.plugins.emit('onFileValidate', {
1,206✔
886
                        program: this,
887
                        file: file
888
                    });
889
                    //call file.validate() IF the file has that function defined
890
                    file.validate?.();
1,206!
891
                    file.isValidated = true;
1,205✔
892

893
                    this.plugins.emit('afterFileValidate', file);
1,205✔
894
                }
895
            })
896
            .forEach(Object.values(this.scopes), (scope) => {
897
                scope.linkSymbolTable();
2,049✔
898
                scope.validate();
2,049✔
899
                scope.unlinkSymbolTable();
2,047✔
900
            })
901
            .once(() => {
902
                this.detectDuplicateComponentNames();
978✔
903
            })
904
            .onCancel(() => {
905
                timeEnd('cancelled');
8✔
906
            })
907
            .onSuccess(() => {
908
                timeEnd();
978✔
909
            })
910
            .onComplete(() => {
911
                //if we emitted the beforeProgramValidate hook, emit the afterProgramValidate hook as well
912
                if (beforeProgramValidateWasEmitted) {
986✔
913
                    const wasCancelled = options?.cancellationToken?.isCancellationRequested ?? false;
983✔
914
                    this.plugins.emit('afterProgramValidate', this, wasCancelled);
983✔
915
                }
916

917
                //regardless of the success of the validation, mark this run as complete
918
                deferred.resolve();
986✔
919
                //clear the validatePromise which means we're no longer running a validation
920
                this.validatePromise = undefined;
986✔
921
            });
922

923
        //run the sequencer in async mode if enabled
924
        if (options?.async) {
986✔
925
            return sequencer.run();
158✔
926

927
            //run the sequencer in sync mode
928
        } else {
929
            return sequencer.runSync();
828✔
930
        }
931
    }
932

933
    /**
934
     * Flag all duplicate component names
935
     */
936
    private detectDuplicateComponentNames() {
937
        const componentsByName = Object.keys(this.files).reduce<Record<string, XmlFile[]>>((map, filePath) => {
978✔
938
            const file = this.files[filePath];
1,260✔
939
            //if this is an XmlFile, and it has a valid `componentName` property
940
            if (isXmlFile(file) && file.componentName?.text) {
1,260✔
941
                let lowerName = file.componentName.text.toLowerCase();
241✔
942
                if (!map[lowerName]) {
241✔
943
                    map[lowerName] = [];
238✔
944
                }
945
                map[lowerName].push(file);
241✔
946
            }
947
            return map;
1,260✔
948
        }, {});
949

950
        for (let name in componentsByName) {
978✔
951
            const xmlFiles = componentsByName[name];
238✔
952
            //add diagnostics for every duplicate component with this name
953
            if (xmlFiles.length > 1) {
238✔
954
                for (let xmlFile of xmlFiles) {
3✔
955
                    const { componentName } = xmlFile;
6✔
956
                    this.diagnostics.push({
6✔
957
                        ...DiagnosticMessages.duplicateComponentName(componentName.text),
958
                        range: xmlFile.componentName.range,
959
                        file: xmlFile,
960
                        relatedInformation: xmlFiles.filter(x => x !== xmlFile).map(x => {
12✔
961
                            return {
6✔
962
                                location: util.createLocation(
963
                                    URI.file(x.srcPath ?? xmlFile.srcPath).toString(),
18!
964
                                    x.componentName.range
965
                                ),
966
                                message: 'Also defined here'
967
                            };
968
                        })
969
                    });
970
                }
971
            }
972
        }
973
    }
974

975
    /**
976
     * Get the files for a list of filePaths
977
     * @param filePaths can be an array of srcPath or a destPath strings
978
     * @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
979
     */
980
    public getFiles<T extends BscFile>(filePaths: string[], normalizePath = true) {
28✔
981
        return filePaths
28✔
982
            .map(filePath => this.getFile(filePath, normalizePath))
30✔
983
            .filter(file => file !== undefined) as T[];
30✔
984
    }
985

986
    /**
987
     * Get the file at the given path
988
     * @param filePath can be a srcPath or a destPath
989
     * @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
990
     */
991
    public getFile<T extends BscFile>(filePath: string, normalizePath = true) {
6,720✔
992
        if (typeof filePath !== 'string') {
8,584✔
993
            return undefined;
1,848✔
994
        } else if (path.isAbsolute(filePath)) {
6,736✔
995
            return this.files[
3,500✔
996
                (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase()
3,500✔
997
            ] as T;
998
        } else {
999
            return this.pkgMap[
3,236✔
1000
                (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase()
3,236!
1001
            ] as T;
1002
        }
1003
    }
1004

1005
    /**
1006
     * Get a list of all scopes the file is loaded into
1007
     * @param file the file
1008
     */
1009
    public getScopesForFile(file: XmlFile | BrsFile | string) {
1010

1011
        const resolvedFile = typeof file === 'string' ? this.getFile(file) : file;
592✔
1012

1013
        let result = [] as Scope[];
592✔
1014
        if (resolvedFile) {
592✔
1015
            for (let key in this.scopes) {
591✔
1016
                let scope = this.scopes[key];
1,232✔
1017

1018
                if (scope.hasFile(resolvedFile)) {
1,232✔
1019
                    result.push(scope);
602✔
1020
                }
1021
            }
1022
        }
1023
        return result;
592✔
1024
    }
1025

1026
    /**
1027
     * Get the first found scope for a file.
1028
     */
1029
    public getFirstScopeForFile(file: XmlFile | BrsFile): Scope | undefined {
1030
        for (let key in this.scopes) {
2,832✔
1031
            let scope = this.scopes[key];
7,017✔
1032

1033
            if (scope.hasFile(file)) {
7,017✔
1034
                return scope;
2,691✔
1035
            }
1036
        }
1037
    }
1038

1039
    public getStatementsByName(name: string, originFile: BrsFile, namespaceName?: string): FileLink<Statement>[] {
1040
        let results = new Map<Statement, FileLink<Statement>>();
39✔
1041
        const filesSearched = new Set<BrsFile>();
39✔
1042
        let lowerNamespaceName = namespaceName?.toLowerCase();
39✔
1043
        let lowerName = name?.toLowerCase();
39!
1044
        //look through all files in scope for matches
1045
        for (const scope of this.getScopesForFile(originFile)) {
39✔
1046
            for (const file of scope.getAllFiles()) {
39✔
1047
                if (isXmlFile(file) || filesSearched.has(file)) {
45✔
1048
                    continue;
3✔
1049
                }
1050
                filesSearched.add(file);
42✔
1051

1052
                for (const statement of [...file.parser.references.functionStatements, ...file.parser.references.classStatements.flatMap((cs) => cs.methods)]) {
42✔
1053
                    let parentNamespaceName = statement.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(originFile.parseMode)?.toLowerCase();
98✔
1054
                    if (statement.name.text.toLowerCase() === lowerName && (!lowerNamespaceName || parentNamespaceName === lowerNamespaceName)) {
98✔
1055
                        if (!results.has(statement)) {
36!
1056
                            results.set(statement, { item: statement, file: file });
36✔
1057
                        }
1058
                    }
1059
                }
1060
            }
1061
        }
1062
        return [...results.values()];
39✔
1063
    }
1064

1065
    public getStatementsForXmlFile(scope: XmlScope, filterName?: string): FileLink<FunctionStatement>[] {
1066
        let results = new Map<Statement, FileLink<FunctionStatement>>();
8✔
1067
        const filesSearched = new Set<BrsFile>();
8✔
1068

1069
        //get all function names for the xml file and parents
1070
        let funcNames = new Set<string>();
8✔
1071
        let currentScope = scope;
8✔
1072
        while (isXmlScope(currentScope)) {
8✔
1073
            for (let name of currentScope.xmlFile.ast.component.api?.functions.map((f) => f.name) ?? []) {
14✔
1074
                if (!filterName || name === filterName) {
14!
1075
                    funcNames.add(name);
14✔
1076
                }
1077
            }
1078
            currentScope = currentScope.getParentScope() as XmlScope;
10✔
1079
        }
1080

1081
        //look through all files in scope for matches
1082
        for (const file of scope.getOwnFiles()) {
8✔
1083
            if (isXmlFile(file) || filesSearched.has(file)) {
16✔
1084
                continue;
8✔
1085
            }
1086
            filesSearched.add(file);
8✔
1087

1088
            for (const statement of file.parser.references.functionStatements) {
8✔
1089
                if (funcNames.has(statement.name.text)) {
13!
1090
                    if (!results.has(statement)) {
13!
1091
                        results.set(statement, { item: statement, file: file });
13✔
1092
                    }
1093
                }
1094
            }
1095
        }
1096
        return [...results.values()];
8✔
1097
    }
1098

1099
    /**
1100
     * Find all available completion items at the given position
1101
     * @param filePath can be a srcPath or a destPath
1102
     * @param position the position (line & column) where completions should be found
1103
     */
1104
    public getCompletions(filePath: string, position: Position) {
1105
        let file = this.getFile(filePath);
77✔
1106
        if (!file) {
77!
1107
            return [];
×
1108
        }
1109

1110
        //find the scopes for this file
1111
        let scopes = this.getScopesForFile(file);
77✔
1112

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

1116
        const event: ProvideCompletionsEvent = {
77✔
1117
            program: this,
1118
            file: file,
1119
            scopes: scopes,
1120
            position: position,
1121
            completions: []
1122
        };
1123

1124
        this.plugins.emit('beforeProvideCompletions', event);
77✔
1125

1126
        this.plugins.emit('provideCompletions', event);
77✔
1127

1128
        this.plugins.emit('afterProvideCompletions', event);
77✔
1129

1130
        return event.completions;
77✔
1131
    }
1132

1133
    /**
1134
     * Goes through each file and builds a list of workspace symbols for the program. Used by LanguageServer's onWorkspaceSymbol functionality
1135
     */
1136
    public getWorkspaceSymbols() {
1137
        const event: ProvideWorkspaceSymbolsEvent = {
22✔
1138
            program: this,
1139
            workspaceSymbols: []
1140
        };
1141
        this.plugins.emit('beforeProvideWorkspaceSymbols', event);
22✔
1142
        this.plugins.emit('provideWorkspaceSymbols', event);
22✔
1143
        this.plugins.emit('afterProvideWorkspaceSymbols', event);
22✔
1144
        return event.workspaceSymbols;
22✔
1145
    }
1146

1147
    /**
1148
     * Given a position in a file, if the position is sitting on some type of identifier,
1149
     * go to the definition of that identifier (where this thing was first defined)
1150
     */
1151
    public getDefinition(srcPath: string, position: Position): Location[] {
1152
        let file = this.getFile(srcPath);
19✔
1153
        if (!file) {
19!
1154
            return [];
×
1155
        }
1156

1157
        const event: ProvideDefinitionEvent = {
19✔
1158
            program: this,
1159
            file: file,
1160
            position: position,
1161
            definitions: []
1162
        };
1163

1164
        this.plugins.emit('beforeProvideDefinition', event);
19✔
1165
        this.plugins.emit('provideDefinition', event);
19✔
1166
        this.plugins.emit('afterProvideDefinition', event);
19✔
1167
        return event.definitions;
19✔
1168
    }
1169

1170
    /**
1171
     * Get hover information for a file and position
1172
     */
1173
    public getHover(srcPath: string, position: Position): Hover[] {
1174
        let file = this.getFile(srcPath);
30✔
1175
        let result: Hover[];
1176
        if (file) {
30!
1177
            const event = {
30✔
1178
                program: this,
1179
                file: file,
1180
                position: position,
1181
                scopes: this.getScopesForFile(file),
1182
                hovers: []
1183
            } as ProvideHoverEvent;
1184
            this.plugins.emit('beforeProvideHover', event);
30✔
1185
            this.plugins.emit('provideHover', event);
30✔
1186
            this.plugins.emit('afterProvideHover', event);
30✔
1187
            result = event.hovers;
30✔
1188
        }
1189

1190
        return result ?? [];
30!
1191
    }
1192

1193
    /**
1194
     * Get full list of document symbols for a file
1195
     * @param srcPath path to the file
1196
     */
1197
    public getDocumentSymbols(srcPath: string): DocumentSymbol[] | undefined {
1198
        let file = this.getFile(srcPath);
24✔
1199
        if (file) {
24!
1200
            const event: ProvideDocumentSymbolsEvent = {
24✔
1201
                program: this,
1202
                file: file,
1203
                documentSymbols: []
1204
            };
1205
            this.plugins.emit('beforeProvideDocumentSymbols', event);
24✔
1206
            this.plugins.emit('provideDocumentSymbols', event);
24✔
1207
            this.plugins.emit('afterProvideDocumentSymbols', event);
24✔
1208
            return event.documentSymbols;
24✔
1209
        } else {
1210
            return undefined;
×
1211
        }
1212
    }
1213

1214
    /**
1215
     * Get the selection ranges for the given positions in a file. Used for expand/shrink selection.
1216
     * @param srcPath path to the file
1217
     * @param positions the positions to get selection ranges for
1218
     */
1219
    public getSelectionRanges(srcPath: string, positions: Position[]): SelectionRange[] {
1220
        const file = this.getFile(srcPath);
15✔
1221
        if (file) {
15✔
1222
            const event: ProvideSelectionRangesEvent = {
14✔
1223
                program: this,
1224
                file: file,
1225
                positions: positions,
1226
                selectionRanges: []
1227
            };
1228
            this.plugins.emit('beforeProvideSelectionRanges', event);
14✔
1229
            this.plugins.emit('provideSelectionRanges', event);
14✔
1230
            this.plugins.emit('afterProvideSelectionRanges', event);
14✔
1231
            return event.selectionRanges;
14✔
1232
        }
1233
        return [];
1✔
1234
    }
1235

1236
    /**
1237
     * Compute code actions for the given file and range
1238
     */
1239
    public getCodeActions(srcPath: string, range: Range) {
1240
        const codeActions = [] as CodeAction[];
52✔
1241
        const file = this.getFile(srcPath);
52✔
1242
        if (file) {
52✔
1243
            const diagnostics = this
51✔
1244
                //get all current diagnostics (filtered by diagnostic filters)
1245
                .getDiagnostics()
1246
                //only keep diagnostics related to this file
1247
                .filter(x => x.file === file)
89✔
1248
                //only keep diagnostics that touch this range
1249
                .filter(x => util.rangesIntersectOrTouch(x.range, range));
66✔
1250

1251
            const scopes = this.getScopesForFile(file);
51✔
1252

1253
            this.plugins.emit('onGetCodeActions', {
51✔
1254
                program: this,
1255
                file: file,
1256
                range: range,
1257
                diagnostics: diagnostics,
1258
                scopes: scopes,
1259
                codeActions: codeActions
1260
            });
1261
        }
1262
        return codeActions;
52✔
1263
    }
1264

1265
    /**
1266
     * Compute "source fix all" code actions for the given file.
1267
     * Fires the `onGetSourceFixAllCodeActions` plugin event with all diagnostics for the file (no range filter),
1268
     * then converts each contributed SourceFixAllCodeAction into an LSP CodeAction.
1269
     */
1270
    public getSourceFixAllCodeActions(srcPath: string): CodeAction[] {
NEW
1271
        const actions: SourceFixAllCodeAction[] = [];
×
NEW
1272
        const file = this.getFile(srcPath);
×
NEW
1273
        if (file) {
×
NEW
1274
            const diagnostics = this
×
1275
                .getDiagnostics()
NEW
1276
                .filter(x => x.file === file);
×
NEW
1277
            const scopes = this.getScopesForFile(file);
×
NEW
1278
            this.plugins.emit('onGetSourceFixAllCodeActions', {
×
1279
                program: this,
1280
                file: file,
1281
                diagnostics: diagnostics,
1282
                scopes: scopes,
1283
                actions: actions
1284
            } as OnGetSourceFixAllCodeActionsEvent);
1285
        }
NEW
1286
        return actions.map(action => codeActionUtil.createCodeAction({
×
1287
            ...action,
1288
            kind: action.kind ?? 'source.fixAll.brighterscript' as any
×
1289
        }));
1290
    }
1291

1292
    /**
1293
     * Get semantic tokens for the specified file
1294
     */
1295
    public getSemanticTokens(srcPath: string): SemanticToken[] | undefined {
1296
        const file = this.getFile(srcPath);
16✔
1297
        if (file) {
16!
1298
            const result = [] as SemanticToken[];
16✔
1299
            this.plugins.emit('onGetSemanticTokens', {
16✔
1300
                program: this,
1301
                file: file,
1302
                scopes: this.getScopesForFile(file),
1303
                semanticTokens: result
1304
            });
1305
            return result;
16✔
1306
        }
1307
    }
1308

1309
    public getSignatureHelp(filePath: string, position: Position): SignatureInfoObj[] {
1310
        let file: BrsFile = this.getFile(filePath);
188✔
1311
        if (!file || !isBrsFile(file)) {
188✔
1312
            return [];
3✔
1313
        }
1314
        let callExpressionInfo = new CallExpressionInfo(file, position);
185✔
1315
        let signatureHelpUtil = new SignatureHelpUtil();
185✔
1316
        return signatureHelpUtil.getSignatureHelpItems(callExpressionInfo);
185✔
1317
    }
1318

1319
    public getReferences(srcPath: string, position: Position): Location[] {
1320
        //find the file
1321
        let file = this.getFile(srcPath);
4✔
1322
        if (!file) {
4!
1323
            return null;
×
1324
        }
1325

1326
        const event: ProvideReferencesEvent = {
4✔
1327
            program: this,
1328
            file: file,
1329
            position: position,
1330
            references: []
1331
        };
1332

1333
        this.plugins.emit('beforeProvideReferences', event);
4✔
1334
        this.plugins.emit('provideReferences', event);
4✔
1335
        this.plugins.emit('afterProvideReferences', event);
4✔
1336

1337
        return event.references;
4✔
1338
    }
1339

1340
    /**
1341
     * Get a list of all script imports, relative to the specified pkgPath
1342
     * @param sourcePkgPath - the pkgPath of the source that wants to resolve script imports.
1343
     */
1344
    public getScriptImportCompletions(sourcePkgPath: string, scriptImport: FileReference) {
1345
        let lowerSourcePkgPath = sourcePkgPath.toLowerCase();
3✔
1346

1347
        let result = [] as CompletionItem[];
3✔
1348
        /**
1349
         * hashtable to prevent duplicate results
1350
         */
1351
        let resultPkgPaths = {} as Record<string, boolean>;
3✔
1352

1353
        //restrict to only .brs files
1354
        for (let key in this.files) {
3✔
1355
            let file = this.files[key];
4✔
1356
            if (
4✔
1357
                //is a BrightScript or BrighterScript file
1358
                (file.extension === '.bs' || file.extension === '.brs') &&
11✔
1359
                //this file is not the current file
1360
                lowerSourcePkgPath !== file.pkgPath.toLowerCase()
1361
            ) {
1362
                //add the relative path
1363
                let relativePath = util.getRelativePath(sourcePkgPath, file.pkgPath).replace(/\\/g, '/');
3✔
1364
                let pkgPathStandardized = file.pkgPath.replace(/\\/g, '/');
3✔
1365
                let filePkgPath = `pkg:/${pkgPathStandardized}`;
3✔
1366
                let lowerFilePkgPath = filePkgPath.toLowerCase();
3✔
1367
                if (!resultPkgPaths[lowerFilePkgPath]) {
3!
1368
                    resultPkgPaths[lowerFilePkgPath] = true;
3✔
1369

1370
                    result.push({
3✔
1371
                        label: relativePath,
1372
                        detail: file.srcPath,
1373
                        kind: CompletionItemKind.File,
1374
                        textEdit: {
1375
                            newText: relativePath,
1376
                            range: scriptImport.filePathRange
1377
                        }
1378
                    });
1379

1380
                    //add the absolute path
1381
                    result.push({
3✔
1382
                        label: filePkgPath,
1383
                        detail: file.srcPath,
1384
                        kind: CompletionItemKind.File,
1385
                        textEdit: {
1386
                            newText: filePkgPath,
1387
                            range: scriptImport.filePathRange
1388
                        }
1389
                    });
1390
                }
1391
            }
1392
        }
1393
        return result;
3✔
1394
    }
1395

1396
    /**
1397
     * Transpile a single file and get the result as a string.
1398
     * This does not write anything to the file system.
1399
     *
1400
     * This should only be called by `LanguageServer`.
1401
     * Internal usage should call `_getTranspiledFileContents` instead.
1402
     * @param filePath can be a srcPath or a destPath
1403
     */
1404
    public async getTranspiledFileContents(filePath: string) {
1405
        const file = this.getFile(filePath);
4✔
1406
        const fileMap: FileObj[] = [{
4✔
1407
            src: file.srcPath,
1408
            dest: file.pkgPath
1409
        }];
1410
        const { entries, astEditor } = this.beforeProgramTranspile(fileMap, this.options.stagingDir);
4✔
1411
        const result = this._getTranspiledFileContents(
4✔
1412
            file
1413
        );
1414
        await this._chainInputSourceMap(result, file);
4✔
1415
        this.afterProgramTranspile(entries, astEditor);
4✔
1416
        return result;
4✔
1417
    }
1418

1419
    /**
1420
     * Internal function used to transpile files.
1421
     * This does not write anything to the file system
1422
     */
1423
    private _getTranspiledFileContents(file: BscFile, outputPath?: string): FileTranspileResult {
1424
        const editor = new AstEditor();
343✔
1425
        this.plugins.emit('beforeFileTranspile', {
343✔
1426
            program: this,
1427
            file: file,
1428
            outputPath: outputPath,
1429
            editor: editor
1430
        });
1431

1432
        //if we have any edits, assume the file needs to be transpiled
1433
        if (editor.hasChanges) {
343✔
1434
            //use the `editor` because it'll track the previous value for us and revert later on
1435
            editor.setProperty(file, 'needsTranspiled', true);
82✔
1436
        }
1437

1438
        //transpile the file
1439
        const result = file.transpile();
343✔
1440

1441
        //generate the typedef if enabled
1442
        let typedef: string;
1443
        if (isBrsFile(file) && this.options.emitDefinitions) {
343✔
1444
            typedef = file.getTypedef();
2✔
1445
        }
1446

1447
        const event: AfterFileTranspileEvent = {
343✔
1448
            program: this,
1449
            file: file,
1450
            outputPath: outputPath,
1451
            editor: editor,
1452
            code: result.code,
1453
            map: result.map,
1454
            typedef: typedef
1455
        };
1456
        this.plugins.emit('afterFileTranspile', event);
343✔
1457

1458
        //undo all `editor` edits that may have been applied to this file.
1459
        editor.undoAll();
343✔
1460

1461
        return {
343✔
1462
            srcPath: file.srcPath,
1463
            pkgPath: file.pkgPath,
1464
            code: event.code,
1465
            map: event.map,
1466
            typedef: event.typedef
1467
        };
1468
    }
1469

1470
    /**
1471
     * If the file has an incoming sourcemap (from a prebuild step), chain it into the
1472
     * generated sourcemap so the output map traces all the way back to the original source.
1473
     * This is async because SourceMapConsumer requires async initialisation in source-map v0.7.
1474
     */
1475
    private async _chainInputSourceMap(result: FileTranspileResult, file: BscFile): Promise<void> {
1476
        if (result.map) {
47✔
1477
            const inputMap = await util.resolveInputSourceMap(file.fileContents ?? '', file.srcPath);
15!
1478
            if (inputMap) {
15✔
1479
                await util.applySourceMap(result.map, inputMap, file.srcPath);
8✔
1480
            }
1481
        }
1482
    }
1483

1484
    private beforeProgramTranspile(fileEntries: FileObj[], stagingDir: string) {
1485
        // map fileEntries using their path as key, to avoid excessive "find()" operations
1486
        const mappedFileEntries = fileEntries.reduce<Record<string, FileObj>>((collection, entry) => {
47✔
1487
            collection[s`${entry.src}`] = entry;
30✔
1488
            return collection;
30✔
1489
        }, {});
1490

1491
        const getOutputPath = (file: BscFile) => {
47✔
1492
            let filePathObj = mappedFileEntries[s`${file.srcPath}`];
97✔
1493
            if (!filePathObj) {
97✔
1494
                //this file has been added in-memory, from a plugin, for example
1495
                filePathObj = {
47✔
1496
                    //add an interpolated src path (since it doesn't actually exist in memory)
1497
                    src: `bsc:/${file.pkgPath}`,
1498
                    dest: file.pkgPath
1499
                };
1500
            }
1501
            //replace the file extension
1502
            let outputPath = filePathObj.dest.replace(/\.bs$/gi, '.brs');
97✔
1503
            //prepend the staging folder path
1504
            outputPath = s`${stagingDir}/${outputPath}`;
97✔
1505
            return outputPath;
97✔
1506
        };
1507

1508
        const entries = Object.values(this.files)
47✔
1509
            //only include the files from fileEntries
1510
            .filter(file => !!mappedFileEntries[file.srcPath])
49✔
1511
            .map(file => {
1512
                return {
28✔
1513
                    file: file,
1514
                    outputPath: getOutputPath(file)
1515
                };
1516
            })
1517
            //sort the entries to make transpiling more deterministic
1518
            .sort((a, b) => {
1519
                return a.file.srcPath < b.file.srcPath ? -1 : 1;
6✔
1520
            });
1521

1522
        const astEditor = new AstEditor();
47✔
1523

1524
        this.plugins.emit('beforeProgramTranspile', this, entries, astEditor);
47✔
1525
        return {
47✔
1526
            entries: entries,
1527
            getOutputPath: getOutputPath,
1528
            astEditor: astEditor
1529
        };
1530
    }
1531

1532
    public async transpile(fileEntries: FileObj[], stagingDir: string) {
1533
        const { entries, getOutputPath, astEditor } = this.beforeProgramTranspile(fileEntries, stagingDir);
42✔
1534

1535
        const processedFiles = new Set<string>();
42✔
1536

1537
        const transpileFile = async (srcPath: string, outputPath?: string) => {
42✔
1538
            //find the file in the program
1539
            const file = this.getFile(srcPath);
45✔
1540
            //mark this file as processed so we don't process it more than once
1541
            processedFiles.add(outputPath?.toLowerCase());
45!
1542

1543
            if (!this.options.pruneEmptyCodeFiles || !file.canBePruned) {
45✔
1544
                //skip transpiling typedef files
1545
                if (isBrsFile(file) && file.isTypedef) {
44✔
1546
                    return;
1✔
1547
                }
1548

1549
                const fileTranspileResult = this._getTranspiledFileContents(file, outputPath);
43✔
1550
                await this._chainInputSourceMap(fileTranspileResult, file);
43✔
1551

1552
                //make sure the full dir path exists
1553
                await fsExtra.ensureDir(path.dirname(outputPath));
43✔
1554

1555
                if (await fsExtra.pathExists(outputPath)) {
43!
1556
                    throw new Error(`Error while transpiling "${file.srcPath}". A file already exists at "${outputPath}" and will not be overwritten.`);
×
1557
                }
1558
                const writeMapPromise = fileTranspileResult.map ? fsExtra.writeFile(`${outputPath}.map`, fileTranspileResult.map.toString()) : null;
43✔
1559
                await Promise.all([
43✔
1560
                    fsExtra.writeFile(outputPath, fileTranspileResult.code),
1561
                    writeMapPromise
1562
                ]);
1563

1564
                if (fileTranspileResult.typedef) {
43✔
1565
                    const typedefPath = outputPath.replace(/\.brs$/i, '.d.bs');
2✔
1566
                    await fsExtra.writeFile(typedefPath, fileTranspileResult.typedef);
2✔
1567
                }
1568
            }
1569
        };
1570

1571
        let promises = entries.map(async (entry) => {
42✔
1572
            return transpileFile(entry?.file?.srcPath, entry.outputPath);
22!
1573
        });
1574

1575
        //if there's no bslib file already loaded into the program, copy it to the staging directory
1576
        if (!this.getFile(bslibAliasedRokuModulesPkgPath) && !this.getFile(s`source/bslib.brs`)) {
42✔
1577
            promises.push(util.copyBslibToStaging(stagingDir, this.options.bslibDestinationDir));
41✔
1578
        }
1579
        await Promise.all(promises);
42✔
1580

1581
        //transpile any new files that plugins added since the start of this transpile process
1582
        do {
42✔
1583
            promises = [];
62✔
1584
            for (const key in this.files) {
62✔
1585
                const file = this.files[key];
69✔
1586
                //this is a new file
1587
                const outputPath = getOutputPath(file);
69✔
1588
                if (!processedFiles.has(outputPath?.toLowerCase())) {
69!
1589
                    promises.push(
23✔
1590
                        transpileFile(file?.srcPath, outputPath)
69!
1591
                    );
1592
                }
1593
            }
1594
            if (promises.length > 0) {
62✔
1595
                this.logger.info(`Transpiling ${promises.length} new files`);
20✔
1596
                await Promise.all(promises);
20✔
1597
            }
1598
        }
1599
        while (promises.length > 0);
1600
        this.afterProgramTranspile(entries, astEditor);
42✔
1601
    }
1602

1603
    private afterProgramTranspile(entries: TranspileObj[], astEditor: AstEditor) {
1604
        this.plugins.emit('afterProgramTranspile', this, entries, astEditor);
46✔
1605
        astEditor.undoAll();
46✔
1606
    }
1607

1608
    /**
1609
     * Find a list of files in the program that have a function with the given name (case INsensitive)
1610
     */
1611
    public findFilesForFunction(functionName: string) {
1612
        const files = [] as BscFile[];
33✔
1613
        const lowerFunctionName = functionName.toLowerCase();
33✔
1614
        //find every file with this function defined
1615
        for (const file of Object.values(this.files)) {
33✔
1616
            if (isBrsFile(file)) {
123✔
1617
                //TODO handle namespace-relative function calls
1618
                //if the file has a function with this name
1619
                if (file.parser.references.functionStatementLookup.get(lowerFunctionName) !== undefined) {
88✔
1620
                    files.push(file);
24✔
1621
                }
1622
            }
1623
        }
1624
        return files;
33✔
1625
    }
1626

1627
    /**
1628
     * Find a list of files in the program that have a class with the given name (case INsensitive)
1629
     */
1630
    public findFilesForClass(className: string) {
1631
        const files = [] as BscFile[];
33✔
1632
        const lowerClassName = className.toLowerCase();
33✔
1633
        //find every file with this class defined
1634
        for (const file of Object.values(this.files)) {
33✔
1635
            if (isBrsFile(file)) {
123✔
1636
                //TODO handle namespace-relative classes
1637
                //if the file has a function with this name
1638
                if (file.parser.references.classStatementLookup.get(lowerClassName) !== undefined) {
88✔
1639
                    files.push(file);
3✔
1640
                }
1641
            }
1642
        }
1643
        return files;
33✔
1644
    }
1645

1646
    public findFilesForNamespace(name: string) {
1647
        const files = [] as BscFile[];
33✔
1648
        const lowerName = name.toLowerCase();
33✔
1649
        //find every file with this class defined
1650
        for (const file of Object.values(this.files)) {
33✔
1651
            if (isBrsFile(file)) {
123✔
1652
                if (file.parser.references.namespaceStatements.find((x) => {
88✔
1653
                    const namespaceName = x.name.toLowerCase();
14✔
1654
                    return (
14✔
1655
                        //the namespace name matches exactly
1656
                        namespaceName === lowerName ||
18✔
1657
                        //the full namespace starts with the name (honoring the part boundary)
1658
                        namespaceName.startsWith(lowerName + '.')
1659
                    );
1660
                })) {
1661
                    files.push(file);
12✔
1662
                }
1663
            }
1664
        }
1665
        return files;
33✔
1666
    }
1667

1668
    public findFilesForEnum(name: string) {
1669
        const files = [] as BscFile[];
34✔
1670
        const lowerName = name.toLowerCase();
34✔
1671
        //find every file with this class defined
1672
        for (const file of Object.values(this.files)) {
34✔
1673
            if (isBrsFile(file)) {
124✔
1674
                if (file.parser.references.enumStatementLookup.get(lowerName)) {
89✔
1675
                    files.push(file);
1✔
1676
                }
1677
            }
1678
        }
1679
        return files;
34✔
1680
    }
1681

1682
    private _manifest: Map<string, string>;
1683

1684
    /**
1685
     * Modify a parsed manifest map by reading `bs_const` and injecting values from `options.manifest.bs_const`
1686
     * @param parsedManifest The manifest map to read from and modify
1687
     */
1688
    private buildBsConstsIntoParsedManifest(parsedManifest: Map<string, string>) {
1689
        // Lift the bs_consts defined in the manifest
1690
        let bsConsts = getBsConst(parsedManifest, false);
19✔
1691

1692
        // Override or delete any bs_consts defined in the bs config
1693
        for (const key in this.options?.manifest?.bs_const) {
19!
1694
            const value = this.options.manifest.bs_const[key];
3✔
1695
            if (value === null) {
3✔
1696
                bsConsts.delete(key);
1✔
1697
            } else {
1698
                bsConsts.set(key, value);
2✔
1699
            }
1700
        }
1701

1702
        // convert the new list of bs consts back into a string for the rest of the down stream systems to use
1703
        let constString = '';
19✔
1704
        for (const [key, value] of bsConsts) {
19✔
1705
            constString += `${constString !== '' ? ';' : ''}${key}=${value.toString()}`;
6✔
1706
        }
1707

1708
        // Set the updated bs_const value
1709
        parsedManifest.set('bs_const', constString);
19✔
1710
    }
1711

1712
    /**
1713
     * Try to find and load the manifest into memory
1714
     * @param manifestFileObj A pointer to a potential manifest file object found during loading
1715
     * @param replaceIfAlreadyLoaded should we overwrite the internal `_manifest` if it already exists
1716
     */
1717
    public loadManifest(manifestFileObj?: FileObj, replaceIfAlreadyLoaded = true) {
1,094✔
1718
        //if we already have a manifest instance, and should not replace...then don't replace
1719
        if (!replaceIfAlreadyLoaded && this._manifest) {
1,106!
1720
            return;
×
1721
        }
1722
        let manifestPath = manifestFileObj
1,106✔
1723
            ? manifestFileObj.src
1,106✔
1724
            : path.join(this.options.rootDir, 'manifest');
1725

1726
        try {
1,106✔
1727
            // we only load this manifest once, so do it sync to improve speed downstream
1728
            const contents = fsExtra.readFileSync(manifestPath, 'utf-8');
1,106✔
1729
            const parsedManifest = parseManifest(contents);
19✔
1730
            this.buildBsConstsIntoParsedManifest(parsedManifest);
19✔
1731
            this._manifest = parsedManifest;
19✔
1732
        } catch (e) {
1733
            this._manifest = new Map();
1,087✔
1734
        }
1735
    }
1736

1737
    /**
1738
     * Get a map of the manifest information
1739
     */
1740
    public getManifest() {
1741
        if (!this._manifest) {
1,453✔
1742
            this.loadManifest();
1,093✔
1743
        }
1744
        return this._manifest;
1,453✔
1745
    }
1746

1747
    public dispose() {
1748
        this.plugins.emit('beforeProgramDispose', { program: this });
1,408✔
1749

1750
        for (let filePath in this.files) {
1,408✔
1751
            this.files[filePath].dispose();
1,432✔
1752
        }
1753
        for (let name in this.scopes) {
1,408✔
1754
            this.scopes[name].dispose();
2,800✔
1755
        }
1756
        this.globalScope.dispose();
1,408✔
1757
        this.dependencyGraph.dispose();
1,408✔
1758
    }
1759
}
1760

1761
export interface FileTranspileResult {
1762
    srcPath: string;
1763
    pkgPath: string;
1764
    code: string;
1765
    map: SourceMapGenerator;
1766
    typedef: string;
1767
}
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