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

rokucommunity / brighterscript / #13605

13 Jan 2025 08:51PM UTC coverage: 86.913% (+0.01%) from 86.902%
#13605

push

web-flow
Merge 8c5f8741e into 9d6ef67ba

11976 of 14547 branches covered (82.33%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

103 existing lines in 10 files now uncovered.

12982 of 14169 relevant lines covered (91.62%)

31777.91 hits per line

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

93.32
/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, Position, Range, SignatureInformation, Location, DocumentSymbol } from 'vscode-languageserver';
5
import type { BsConfig, FinalizedBsConfig } from './BsConfig';
6
import { Scope } from './Scope';
1✔
7
import { DiagnosticMessages } from './DiagnosticMessages';
1✔
8
import type { FileObj, SemanticToken, FileLink, ProvideHoverEvent, ProvideCompletionsEvent, Hover, ProvideDefinitionEvent, ProvideReferencesEvent, ProvideDocumentSymbolsEvent, ProvideWorkspaceSymbolsEvent, BeforeFileAddEvent, BeforeFileRemoveEvent, PrepareFileEvent, PrepareProgramEvent, ProvideFileEvent, SerializedFile, TranspileObj, SerializeFileEvent, ExtraSymbolData } from './interfaces';
9
import { standardizePath as s, util } from './util';
1✔
10
import { XmlScope } from './XmlScope';
1✔
11
import { DependencyGraph } from './DependencyGraph';
1✔
12
import type { Logger } from './logging';
13
import { LogLevel, createLogger } from './logging';
1✔
14
import chalk from 'chalk';
1✔
15
import { globalCallables, globalFile } from './globalCallables';
1✔
16
import { parseManifest, getBsConst } from './preprocessor/Manifest';
1✔
17
import { URI } from 'vscode-uri';
1✔
18
import PluginInterface from './PluginInterface';
1✔
19
import { isBrsFile, isXmlFile, isXmlScope, isNamespaceStatement } from './astUtils/reflection';
1✔
20
import type { FunctionStatement, MethodStatement, NamespaceStatement } from './parser/Statement';
21
import { BscPlugin } from './bscPlugin/BscPlugin';
1✔
22
import { Editor } from './astUtils/Editor';
1✔
23
import type { Statement } from './parser/AstNode';
24
import { CallExpressionInfo } from './bscPlugin/CallExpressionInfo';
1✔
25
import { SignatureHelpUtil } from './bscPlugin/SignatureHelpUtil';
1✔
26
import { IntegerType } from './types/IntegerType';
1✔
27
import { StringType } from './types/StringType';
1✔
28
import { SymbolTypeFlag } from './SymbolTypeFlag';
1✔
29
import { BooleanType } from './types/BooleanType';
1✔
30
import { DoubleType } from './types/DoubleType';
1✔
31
import { DynamicType } from './types/DynamicType';
1✔
32
import { FloatType } from './types/FloatType';
1✔
33
import { LongIntegerType } from './types/LongIntegerType';
1✔
34
import { ObjectType } from './types/ObjectType';
1✔
35
import { VoidType } from './types/VoidType';
1✔
36
import { FunctionType } from './types/FunctionType';
1✔
37
import { FileFactory } from './files/Factory';
1✔
38
import { ActionPipeline } from './ActionPipeline';
1✔
39
import type { FileData } from './files/LazyFileData';
40
import { LazyFileData } from './files/LazyFileData';
1✔
41
import { rokuDeploy } from 'roku-deploy';
1✔
42
import type { SGNodeData, BRSComponentData, BRSEventData, BRSInterfaceData } from './roku-types';
43
import { nodes, components, interfaces, events } from './roku-types';
1✔
44
import { ComponentType } from './types/ComponentType';
1✔
45
import { InterfaceType } from './types/InterfaceType';
1✔
46
import { BuiltInInterfaceAdder } from './types/BuiltInInterfaceAdder';
1✔
47
import type { UnresolvedSymbol } from './AstValidationSegmenter';
48
import { WalkMode, createVisitor } from './astUtils/visitors';
1✔
49
import type { BscFile } from './files/BscFile';
50
import { Stopwatch } from './Stopwatch';
1✔
51
import { firstBy } from 'thenby';
1✔
52
import { CrossScopeValidator } from './CrossScopeValidator';
1✔
53
import { DiagnosticManager } from './DiagnosticManager';
1✔
54
import { ProgramValidatorDiagnosticsTag } from './bscPlugin/validation/ProgramValidator';
1✔
55
import type { ProvidedSymbolInfo, BrsFile } from './files/BrsFile';
56
import type { XmlFile } from './files/XmlFile';
57

58
const bslibNonAliasedRokuModulesPkgPath = s`source/roku_modules/rokucommunity_bslib/bslib.brs`;
1✔
59
const bslibAliasedRokuModulesPkgPath = s`source/roku_modules/bslib/bslib.brs`;
1✔
60

61
export interface SignatureInfoObj {
62
    index: number;
63
    key: string;
64
    signature: SignatureInformation;
65
}
66

67
export class Program {
1✔
68
    constructor(
69
        /**
70
         * The root directory for this program
71
         */
72
        options: BsConfig,
73
        logger?: Logger,
74
        plugins?: PluginInterface,
75
        diagnosticsManager?: DiagnosticManager
76
    ) {
77
        this.options = util.normalizeConfig(options);
1,829✔
78
        this.logger = logger ?? createLogger(options);
1,829✔
79
        this.plugins = plugins || new PluginInterface([], { logger: this.logger });
1,829✔
80
        this.diagnostics = diagnosticsManager || new DiagnosticManager();
1,829✔
81

82
        // initialize the diagnostics Manager
83
        this.diagnostics.logger = this.logger;
1,829✔
84
        this.diagnostics.options = this.options;
1,829✔
85
        this.diagnostics.program = this;
1,829✔
86

87
        //inject the bsc plugin as the first plugin in the stack.
88
        this.plugins.addFirst(new BscPlugin());
1,829✔
89

90
        //normalize the root dir path
91
        this.options.rootDir = util.getRootDir(this.options);
1,829✔
92

93
        this.createGlobalScope();
1,829✔
94

95
        this.fileFactory = new FileFactory(this);
1,829✔
96
    }
97

98
    public options: FinalizedBsConfig;
99
    public logger: Logger;
100

101
    /**
102
     * An editor that plugins can use to modify program-level things during the build flow. Don't use this to edit files (they have their own `.editor`)
103
     */
104
    public editor = new Editor();
1,829✔
105

106
    /**
107
     * A factory that creates `File` instances
108
     */
109
    private fileFactory: FileFactory;
110

111
    private createGlobalScope() {
112
        //create the 'global' scope
113
        this.globalScope = new Scope('global', this, 'scope:global');
1,829✔
114
        this.globalScope.attachDependencyGraph(this.dependencyGraph);
1,829✔
115
        this.scopes.global = this.globalScope;
1,829✔
116

117
        this.populateGlobalSymbolTable();
1,829✔
118

119
        //hardcode the files list for global scope to only contain the global file
120
        this.globalScope.getAllFiles = () => [globalFile];
15,591✔
121
        globalFile.isValidated = true;
1,829✔
122
        this.globalScope.validate();
1,829✔
123

124
        //TODO we might need to fix this because the isValidated clears stuff now
125
        (this.globalScope as any).isValidated = true;
1,829✔
126
    }
127

128

129
    private recursivelyAddNodeToSymbolTable(nodeData: SGNodeData) {
130
        if (!nodeData) {
338,365!
UNCOV
131
            return;
×
132
        }
133
        let nodeType: ComponentType;
134
        const nodeName = util.getSgNodeTypeName(nodeData.name);
338,365✔
135
        if (!this.globalScope.symbolTable.hasSymbol(nodeName, SymbolTypeFlag.typetime)) {
338,365✔
136
            let parentNode: ComponentType;
137
            if (nodeData.extends) {
175,584✔
138
                const parentNodeData = nodes[nodeData.extends.name.toLowerCase()];
162,781✔
139
                try {
162,781✔
140
                    parentNode = this.recursivelyAddNodeToSymbolTable(parentNodeData);
162,781✔
141
                } catch (error) {
UNCOV
142
                    this.logger.error(error, nodeData);
×
143
                }
144
            }
145
            nodeType = new ComponentType(nodeData.name, parentNode);
175,584✔
146
            nodeType.addBuiltInInterfaces();
175,584✔
147
            if (nodeData.name === 'Node') {
175,584✔
148
                // Add `roSGNode` as shorthand for `roSGNodeNode`
149
                this.globalScope.symbolTable.addSymbol('roSGNode', { description: nodeData.description, isBuiltIn: true }, nodeType, SymbolTypeFlag.typetime);
1,829✔
150
            }
151
            this.globalScope.symbolTable.addSymbol(nodeName, { description: nodeData.description, isBuiltIn: true }, nodeType, SymbolTypeFlag.typetime);
175,584✔
152
        } else {
153
            nodeType = this.globalScope.symbolTable.getSymbolType(nodeName, { flags: SymbolTypeFlag.typetime }) as ComponentType;
162,781✔
154
        }
155

156
        return nodeType;
338,365✔
157
    }
158
    /**
159
     * Do all setup required for the global symbol table.
160
     */
161
    private populateGlobalSymbolTable() {
162
        //Setup primitive types in global symbolTable
163

164
        const builtInSymbolData: ExtraSymbolData = { isBuiltIn: true };
1,829✔
165

166
        this.globalScope.symbolTable.addSymbol('boolean', builtInSymbolData, BooleanType.instance, SymbolTypeFlag.typetime);
1,829✔
167
        this.globalScope.symbolTable.addSymbol('double', builtInSymbolData, DoubleType.instance, SymbolTypeFlag.typetime);
1,829✔
168
        this.globalScope.symbolTable.addSymbol('dynamic', builtInSymbolData, DynamicType.instance, SymbolTypeFlag.typetime);
1,829✔
169
        this.globalScope.symbolTable.addSymbol('float', builtInSymbolData, FloatType.instance, SymbolTypeFlag.typetime);
1,829✔
170
        this.globalScope.symbolTable.addSymbol('function', builtInSymbolData, new FunctionType(), SymbolTypeFlag.typetime);
1,829✔
171
        this.globalScope.symbolTable.addSymbol('integer', builtInSymbolData, IntegerType.instance, SymbolTypeFlag.typetime);
1,829✔
172
        this.globalScope.symbolTable.addSymbol('longinteger', builtInSymbolData, LongIntegerType.instance, SymbolTypeFlag.typetime);
1,829✔
173
        this.globalScope.symbolTable.addSymbol('object', builtInSymbolData, new ObjectType(), SymbolTypeFlag.typetime);
1,829✔
174
        this.globalScope.symbolTable.addSymbol('string', builtInSymbolData, StringType.instance, SymbolTypeFlag.typetime);
1,829✔
175
        this.globalScope.symbolTable.addSymbol('void', builtInSymbolData, VoidType.instance, SymbolTypeFlag.typetime);
1,829✔
176

177
        BuiltInInterfaceAdder.getLookupTable = () => this.globalScope.symbolTable;
804,117✔
178

179
        for (const callable of globalCallables) {
1,829✔
180
            this.globalScope.symbolTable.addSymbol(callable.name, { ...builtInSymbolData, description: callable.shortDescription }, callable.type, SymbolTypeFlag.runtime);
142,662✔
181
        }
182

183
        for (const ifaceData of Object.values(interfaces) as BRSInterfaceData[]) {
1,829✔
184
            const nodeType = new InterfaceType(ifaceData.name);
160,952✔
185
            nodeType.addBuiltInInterfaces();
160,952✔
186
            this.globalScope.symbolTable.addSymbol(ifaceData.name, { ...builtInSymbolData, description: ifaceData.description }, nodeType, SymbolTypeFlag.typetime);
160,952✔
187
        }
188

189
        for (const componentData of Object.values(components) as BRSComponentData[]) {
1,829✔
190
            const nodeType = new InterfaceType(componentData.name);
118,885✔
191
            nodeType.addBuiltInInterfaces();
118,885✔
192
            if (componentData.name !== 'roSGNode') {
118,885✔
193
                // we will add `roSGNode` as shorthand for `roSGNodeNode`, since all roSgNode components are SceneGraph nodes
194
                this.globalScope.symbolTable.addSymbol(componentData.name, { ...builtInSymbolData, description: componentData.description }, nodeType, SymbolTypeFlag.typetime);
117,056✔
195
            }
196
        }
197

198
        for (const nodeData of Object.values(nodes) as SGNodeData[]) {
1,829✔
199
            this.recursivelyAddNodeToSymbolTable(nodeData);
175,584✔
200
        }
201

202
        for (const eventData of Object.values(events) as BRSEventData[]) {
1,829✔
203
            const nodeType = new InterfaceType(eventData.name);
32,922✔
204
            nodeType.addBuiltInInterfaces();
32,922✔
205
            this.globalScope.symbolTable.addSymbol(eventData.name, { ...builtInSymbolData, description: eventData.description }, nodeType, SymbolTypeFlag.typetime);
32,922✔
206
        }
207

208
    }
209

210
    /**
211
     * A graph of all files and their dependencies.
212
     * For example:
213
     *      File.xml -> [lib1.brs, lib2.brs]
214
     *      lib2.brs -> [lib3.brs] //via an import statement
215
     */
216
    private dependencyGraph = new DependencyGraph();
1,829✔
217

218
    public diagnostics: DiagnosticManager;
219

220
    /**
221
     * A scope that contains all built-in global functions.
222
     * All scopes should directly or indirectly inherit from this scope
223
     */
224
    public globalScope: Scope = undefined as any;
1,829✔
225

226
    /**
227
     * Plugins which can provide extra diagnostics or transform AST
228
     */
229
    public plugins: PluginInterface;
230

231
    private fileSymbolInformation = new Map<string, { provides: ProvidedSymbolInfo; requires: UnresolvedSymbol[] }>();
1,829✔
232

233
    public addFileSymbolInfo(file: BrsFile) {
234
        this.fileSymbolInformation.set(file.pkgPath, {
1,680✔
235
            provides: file.providedSymbols,
236
            requires: file.requiredSymbols
237
        });
238
    }
239

240
    public getFileSymbolInfo(file: BrsFile) {
241
        return this.fileSymbolInformation.get(file.pkgPath);
1,683✔
242
    }
243

244
    /**
245
     * The path to bslib.brs (the BrightScript runtime for certain BrighterScript features)
246
     */
247
    public get bslibPkgPath() {
248
        //if there's an aliased (preferred) version of bslib from roku_modules loaded into the program, use that
249
        if (this.getFile(bslibAliasedRokuModulesPkgPath)) {
2,466✔
250
            return bslibAliasedRokuModulesPkgPath;
11✔
251

252
            //if there's a non-aliased version of bslib from roku_modules, use that
253
        } else if (this.getFile(bslibNonAliasedRokuModulesPkgPath)) {
2,455✔
254
            return bslibNonAliasedRokuModulesPkgPath;
24✔
255

256
            //default to the embedded version
257
        } else {
258
            return `${this.options.bslibDestinationDir}${path.sep}bslib.brs`;
2,431✔
259
        }
260
    }
261

262
    public get bslibPrefix() {
263
        if (this.bslibPkgPath === bslibNonAliasedRokuModulesPkgPath) {
1,797✔
264
            return 'rokucommunity_bslib';
18✔
265
        } else {
266
            return 'bslib';
1,779✔
267
        }
268
    }
269

270

271
    /**
272
     * A map of every file loaded into this program, indexed by its original file location
273
     */
274
    public files = {} as Record<string, BscFile>;
1,829✔
275
    /**
276
     * A map of every file loaded into this program, indexed by its destPath
277
     */
278
    private destMap = new Map<string, BscFile>();
1,829✔
279
    /**
280
     * Plugins can contribute multiple virtual files for a single physical file.
281
     * This collection links the virtual files back to the physical file that produced them.
282
     * The key is the standardized and lower-cased srcPath
283
     */
284
    private fileClusters = new Map<string, BscFile[]>();
1,829✔
285

286
    private scopes = {} as Record<string, Scope>;
1,829✔
287

288
    protected addScope(scope: Scope) {
289
        this.scopes[scope.name] = scope;
1,966✔
290
        delete this.sortedScopeNames;
1,966✔
291
    }
292

293
    protected removeScope(scope: Scope) {
294
        if (this.scopes[scope.name]) {
11!
295
            delete this.scopes[scope.name];
11✔
296
            delete this.sortedScopeNames;
11✔
297
        }
298
    }
299

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

308
    /**
309
     * Get the component with the specified name
310
     */
311
    public getComponent(componentName: string) {
312
        if (componentName) {
1,807✔
313
            //return the first compoment in the list with this name
314
            //(components are ordered in this list by destPath to ensure consistency)
315
            return this.components[componentName.toLowerCase()]?.[0];
1,793✔
316
        } else {
317
            return undefined;
14✔
318
        }
319
    }
320

321
    /**
322
     * Get the sorted names of custom components
323
     */
324
    public getSortedComponentNames() {
325
        const componentNames = Object.keys(this.components);
1,367✔
326
        componentNames.sort((a, b) => {
1,367✔
327
            if (a < b) {
696✔
328
                return -1;
272✔
329
            } else if (b < a) {
424!
330
                return 1;
424✔
331
            }
UNCOV
332
            return 0;
×
333
        });
334
        return componentNames;
1,367✔
335
    }
336

337
    /**
338
     * Keeps a set of all the components that need to have their types updated during the current validation cycle
339
     */
340
    private componentSymbolsToUpdate = new Set<{ componentKey: string; componentName: string }>();
1,829✔
341

342
    /**
343
     * Register (or replace) the reference to a component in the component map
344
     */
345
    private registerComponent(xmlFile: XmlFile, scope: XmlScope) {
346
        const key = this.getComponentKey(xmlFile);
378✔
347
        if (!this.components[key]) {
378✔
348
            this.components[key] = [];
366✔
349
        }
350
        this.components[key].push({
378✔
351
            file: xmlFile,
352
            scope: scope
353
        });
354
        this.components[key].sort((a, b) => {
378✔
355
            const pathA = a.file.destPath.toLowerCase();
5✔
356
            const pathB = b.file.destPath.toLowerCase();
5✔
357
            if (pathA < pathB) {
5✔
358
                return -1;
1✔
359
            } else if (pathA > pathB) {
4!
360
                return 1;
4✔
361
            }
UNCOV
362
            return 0;
×
363
        });
364
        this.syncComponentDependencyGraph(this.components[key]);
378✔
365
        this.addDeferredComponentTypeSymbolCreation(xmlFile);
378✔
366
    }
367

368
    /**
369
     * Remove the specified component from the components map
370
     */
371
    private unregisterComponent(xmlFile: XmlFile) {
372
        const key = this.getComponentKey(xmlFile);
11✔
373
        const arr = this.components[key] || [];
11!
374
        for (let i = 0; i < arr.length; i++) {
11✔
375
            if (arr[i].file === xmlFile) {
11!
376
                arr.splice(i, 1);
11✔
377
                break;
11✔
378
            }
379
        }
380

381
        this.syncComponentDependencyGraph(arr);
11✔
382
        this.addDeferredComponentTypeSymbolCreation(xmlFile);
11✔
383
    }
384

385
    /**
386
     * Adds a component described in an XML to the set of components that needs to be updated this validation cycle.
387
     * @param xmlFile XML file with <component> tag
388
     */
389
    private addDeferredComponentTypeSymbolCreation(xmlFile: XmlFile) {
390
        this.componentSymbolsToUpdate.add({ componentKey: this.getComponentKey(xmlFile), componentName: xmlFile.componentName?.text });
389✔
391

392
    }
393

394
    private getComponentKey(xmlFile: XmlFile) {
395
        return (xmlFile.componentName?.text ?? xmlFile.pkgPath).toLowerCase();
778✔
396
    }
397

398
    /**
399
     * Updates the global symbol table with the first component in this.components to have the same name as the component in the file
400
     * @param componentKey key getting a component from `this.components`
401
     * @param componentName the unprefixed name of the component that will be added (e.g. 'MyLabel' NOT 'roSgNodeMyLabel')
402
     */
403
    private updateComponentSymbolInGlobalScope(componentKey: string, componentName: string) {
404
        const symbolName = componentName ? util.getSgNodeTypeName(componentName) : undefined;
308✔
405
        if (!symbolName) {
308✔
406
            return;
7✔
407
        }
408
        const components = this.components[componentKey] || [];
301!
409
        // Remove any existing symbols that match
410
        this.globalScope.symbolTable.removeSymbol(symbolName);
301✔
411
        // There is a component that can be added - use it.
412
        if (components.length > 0) {
301✔
413
            const componentScope = components[0].scope;
300✔
414
            // TODO: May need to link symbol tables to get correct types for callfuncs
415
            // componentScope.linkSymbolTable();
416
            const componentType = componentScope.getComponentType();
300✔
417
            if (componentType) {
300!
418
                this.globalScope.symbolTable.addSymbol(symbolName, {}, componentType, SymbolTypeFlag.typetime);
300✔
419
            }
420
            // TODO: Remember to unlink! componentScope.unlinkSymbolTable();
421
        }
422
    }
423

424
    /**
425
     * re-attach the dependency graph with a new key for any component who changed
426
     * their position in their own named array (only matters when there are multiple
427
     * components with the same name)
428
     */
429
    private syncComponentDependencyGraph(components: Array<{ file: XmlFile; scope: XmlScope }>) {
430
        //reattach every dependency graph
431
        for (let i = 0; i < components.length; i++) {
389✔
432
            const { file, scope } = components[i];
384✔
433

434
            //attach (or re-attach) the dependencyGraph for every component whose position changed
435
            if (file.dependencyGraphIndex !== i) {
384✔
436
                file.dependencyGraphIndex = i;
380✔
437
                this.dependencyGraph.addOrReplace(file.dependencyGraphKey, file.dependencies);
380✔
438
                file.attachDependencyGraph(this.dependencyGraph);
380✔
439
                scope.attachDependencyGraph(this.dependencyGraph);
380✔
440
            }
441
        }
442
    }
443

444
    /**
445
     * Get a list of all files that are included in the project but are not referenced
446
     * by any scope in the program.
447
     */
448
    public getUnreferencedFiles() {
UNCOV
449
        let result = [] as BscFile[];
×
UNCOV
450
        for (let filePath in this.files) {
×
UNCOV
451
            let file = this.files[filePath];
×
452
            //is this file part of a scope
UNCOV
453
            if (!this.getFirstScopeForFile(file)) {
×
454
                //no scopes reference this file. add it to the list
UNCOV
455
                result.push(file);
×
456
            }
457
        }
UNCOV
458
        return result;
×
459
    }
460

461
    /**
462
     * Get the list of errors for the entire program.
463
     */
464
    public getDiagnostics() {
465
        return this.diagnostics.getDiagnostics();
1,165✔
466
    }
467

468
    /**
469
     * Determine if the specified file is loaded in this program right now.
470
     * @param filePath the absolute or relative path to the file
471
     * @param normalizePath should the provided path be normalized before use
472
     */
473
    public hasFile(filePath: string, normalizePath = true) {
2,548✔
474
        return !!this.getFile(filePath, normalizePath);
2,548✔
475
    }
476

477
    /**
478
     * roku filesystem is case INsensitive, so find the scope by key case insensitive
479
     * @param scopeName xml scope names are their `destPath`. Source scope is stored with the key `"source"`
480
     */
481
    public getScopeByName(scopeName: string): Scope | undefined {
482
        if (!scopeName) {
57!
483
            return undefined;
×
484
        }
485
        //most scopes are xml file pkg paths. however, the ones that are not are single names like "global" and "scope",
486
        //so it's safe to run the standardizePkgPath method
487
        scopeName = s`${scopeName}`;
57✔
488
        let key = Object.keys(this.scopes).find(x => x.toLowerCase() === scopeName.toLowerCase());
131✔
489
        return this.scopes[key!];
57✔
490
    }
491

492
    /**
493
     * Return all scopes
494
     */
495
    public getScopes() {
496
        return Object.values(this.scopes);
12✔
497
    }
498

499
    /**
500
     * Find the scope for the specified component
501
     */
502
    public getComponentScope(componentName: string) {
503
        return this.getComponent(componentName)?.scope;
427✔
504
    }
505

506
    /**
507
     * Update internal maps with this file reference
508
     */
509
    private assignFile<T extends BscFile = BscFile>(file: T) {
510
        const fileAddEvent: BeforeFileAddEvent = {
2,369✔
511
            file: file,
512
            program: this
513
        };
514

515
        this.plugins.emit('beforeFileAdd', fileAddEvent);
2,369✔
516

517
        this.files[file.srcPath.toLowerCase()] = file;
2,369✔
518
        this.destMap.set(file.destPath.toLowerCase(), file);
2,369✔
519

520
        this.plugins.emit('afterFileAdd', fileAddEvent);
2,369✔
521

522
        return file;
2,369✔
523
    }
524

525
    /**
526
     * Remove this file from internal maps
527
     */
528
    private unassignFile<T extends BscFile = BscFile>(file: T) {
529
        delete this.files[file.srcPath.toLowerCase()];
152✔
530
        this.destMap.delete(file.destPath.toLowerCase());
152✔
531
        return file;
152✔
532
    }
533

534
    /**
535
     * Load a file into the program. If that file already exists, it is replaced.
536
     * If file contents are provided, those are used, Otherwise, the file is loaded from the file system
537
     * @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:/`)
538
     * @param fileData the file contents. omit or pass `undefined` to prevent loading the data at this time
539
     */
540
    public setFile<T extends BscFile>(srcDestOrPkgPath: string, fileData?: FileData): T;
541
    /**
542
     * Load a file into the program. If that file already exists, it is replaced.
543
     * @param fileEntry an object that specifies src and dest for the file.
544
     * @param fileData the file contents. omit or pass `undefined` to prevent loading the data at this time
545
     */
546
    public setFile<T extends BscFile>(fileEntry: FileObj, fileData: FileData): T;
547
    public setFile<T extends BscFile>(fileParam: FileObj | string, fileData: FileData): T {
548
        //normalize the file paths
549
        const { srcPath, destPath } = this.getPaths(fileParam, this.options.rootDir);
2,365✔
550

551
        let file = this.logger.time(LogLevel.debug, ['Program.setFile()', chalk.green(srcPath)], () => {
2,365✔
552
            //if the file is already loaded, remove it
553
            if (this.hasFile(srcPath)) {
2,365✔
554
                this.removeFile(srcPath, true, true);
136✔
555
            }
556

557
            const data = new LazyFileData(fileData);
2,365✔
558

559
            const event = new ProvideFileEventInternal(this, srcPath, destPath, data, this.fileFactory);
2,365✔
560

561
            this.plugins.emit('beforeProvideFile', event);
2,365✔
562
            this.plugins.emit('provideFile', event);
2,365✔
563
            this.plugins.emit('afterProvideFile', event);
2,365✔
564

565
            //if no files were provided, create a AssetFile to represent it.
566
            if (event.files.length === 0) {
2,365✔
567
                event.files.push(
18✔
568
                    this.fileFactory.AssetFile({
569
                        srcPath: event.srcPath,
570
                        destPath: event.destPath,
571
                        pkgPath: event.destPath,
572
                        data: data
573
                    })
574
                );
575
            }
576

577
            //find the file instance for the srcPath that triggered this action.
578
            const primaryFile = event.files.find(x => x.srcPath === srcPath);
2,365✔
579

580
            if (!primaryFile) {
2,365!
UNCOV
581
                throw new Error(`No file provided for srcPath '${srcPath}'. Instead, received ${JSON.stringify(event.files.map(x => ({
×
582
                    type: x.type,
583
                    srcPath: x.srcPath,
584
                    destPath: x.destPath
585
                })))}`);
586
            }
587

588
            //link the virtual files to the primary file
589
            this.fileClusters.set(primaryFile.srcPath?.toLowerCase(), event.files);
2,365!
590

591
            for (const file of event.files) {
2,365✔
592
                file.srcPath = s(file.srcPath);
2,369✔
593
                if (file.destPath) {
2,369!
594
                    file.destPath = s`${util.replaceCaseInsensitive(file.destPath, this.options.rootDir, '')}`;
2,369✔
595
                }
596
                if (file.pkgPath) {
2,369✔
597
                    file.pkgPath = s`${util.replaceCaseInsensitive(file.pkgPath, this.options.rootDir, '')}`;
2,365✔
598
                } else {
599
                    file.pkgPath = file.destPath;
4✔
600
                }
601
                file.excludeFromOutput = file.excludeFromOutput === true;
2,369✔
602

603
                //set the dependencyGraph key for every file to its destPath
604
                file.dependencyGraphKey = file.destPath.toLowerCase();
2,369✔
605

606
                this.assignFile(file);
2,369✔
607

608
                //register a callback anytime this file's dependencies change
609
                if (typeof file.onDependenciesChanged === 'function') {
2,369✔
610
                    file.disposables ??= [];
2,343!
611
                    file.disposables.push(
2,343✔
612
                        this.dependencyGraph.onchange(file.dependencyGraphKey, file.onDependenciesChanged.bind(file))
613
                    );
614
                }
615

616
                //register this file (and its dependencies) with the dependency graph
617
                this.dependencyGraph.addOrReplace(file.dependencyGraphKey, file.dependencies ?? []);
2,369✔
618

619
                //if this is a `source` file, add it to the source scope's dependency list
620
                if (this.isSourceBrsFile(file)) {
2,369✔
621
                    this.createSourceScope();
1,638✔
622
                    this.dependencyGraph.addDependency('scope:source', file.dependencyGraphKey);
1,638✔
623
                }
624

625
                //if this is an xml file in the components folder, register it as a component
626
                if (this.isComponentsXmlFile(file)) {
2,369✔
627
                    //create a new scope for this xml file
628
                    let scope = new XmlScope(file, this);
378✔
629
                    this.addScope(scope);
378✔
630

631
                    //register this compoent now that we have parsed it and know its component name
632
                    this.registerComponent(file, scope);
378✔
633

634
                    //notify plugins that the scope is created and the component is registered
635
                    this.plugins.emit('afterScopeCreate', {
378✔
636
                        program: this,
637
                        scope: scope
638
                    });
639
                }
640
            }
641

642
            return primaryFile;
2,365✔
643
        });
644
        return file as T;
2,365✔
645
    }
646

647
    /**
648
     * Given a srcPath, a destPath, or both, resolve whichever is missing, relative to rootDir.
649
     * @param fileParam an object representing file paths
650
     * @param rootDir must be a pre-normalized path
651
     */
652
    private getPaths(fileParam: string | FileObj | { srcPath?: string; pkgPath?: string }, rootDir: string) {
653
        let srcPath: string | undefined;
654
        let destPath: string | undefined;
655

656
        assert.ok(fileParam, 'fileParam is required');
2,520✔
657

658
        //lift the path vars from the incoming param
659
        if (typeof fileParam === 'string') {
2,520✔
660
            fileParam = this.removePkgPrefix(fileParam);
2,158✔
661
            srcPath = s`${path.resolve(rootDir, fileParam)}`;
2,158✔
662
            destPath = s`${util.replaceCaseInsensitive(srcPath, rootDir, '')}`;
2,158✔
663
        } else {
664
            let param: any = fileParam;
362✔
665

666
            if (param.src) {
362✔
667
                srcPath = s`${param.src}`;
361✔
668
            }
669
            if (param.srcPath) {
362!
UNCOV
670
                srcPath = s`${param.srcPath}`;
×
671
            }
672
            if (param.dest) {
362✔
673
                destPath = s`${this.removePkgPrefix(param.dest)}`;
361✔
674
            }
675
            if (param.pkgPath) {
362!
UNCOV
676
                destPath = s`${this.removePkgPrefix(param.pkgPath)}`;
×
677
            }
678
        }
679

680
        //if there's no srcPath, use the destPath to build an absolute srcPath
681
        if (!srcPath) {
2,520✔
682
            srcPath = s`${rootDir}/${destPath}`;
1✔
683
        }
684
        //coerce srcPath to an absolute path
685
        if (!path.isAbsolute(srcPath)) {
2,520✔
686
            srcPath = util.standardizePath(srcPath);
1✔
687
        }
688

689
        //if destPath isn't set, compute it from the other paths
690
        if (!destPath) {
2,520✔
691
            destPath = s`${util.replaceCaseInsensitive(srcPath, rootDir, '')}`;
1✔
692
        }
693

694
        assert.ok(srcPath, 'fileEntry.src is required');
2,520✔
695
        assert.ok(destPath, 'fileEntry.dest is required');
2,520✔
696

697
        return {
2,520✔
698
            srcPath: srcPath,
699
            //remove leading slash
700
            destPath: destPath.replace(/^[\/\\]+/, '')
701
        };
702
    }
703

704
    /**
705
     * Remove any leading `pkg:/` found in the path
706
     */
707
    private removePkgPrefix(path: string) {
708
        return path.replace(/^pkg:\//i, '');
2,519✔
709
    }
710

711
    /**
712
     * Is this file a .brs file found somewhere within the `pkg:/source/` folder?
713
     */
714
    private isSourceBrsFile(file: BscFile) {
715
        return !!/^(pkg:\/)?source[\/\\]/.exec(file.destPath);
2,521✔
716
    }
717

718
    /**
719
     * Is this file a .brs file found somewhere within the `pkg:/source/` folder?
720
     */
721
    private isComponentsXmlFile(file: BscFile): file is XmlFile {
722
        return isXmlFile(file) && !!/^(pkg:\/)?components[\/\\]/.exec(file.destPath);
2,369✔
723
    }
724

725
    /**
726
     * Ensure source scope is created.
727
     * Note: automatically called internally, and no-op if it exists already.
728
     */
729
    public createSourceScope() {
730
        if (!this.scopes.source) {
2,409✔
731
            const sourceScope = new Scope('source', this, 'scope:source');
1,588✔
732
            sourceScope.attachDependencyGraph(this.dependencyGraph);
1,588✔
733
            this.addScope(sourceScope);
1,588✔
734
            this.plugins.emit('afterScopeCreate', {
1,588✔
735
                program: this,
736
                scope: sourceScope
737
            });
738
        }
739
    }
740

741
    /**
742
     * Remove a set of files from the program
743
     * @param srcPaths can be an array of srcPath or destPath strings
744
     * @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
745
     */
746
    public removeFiles(srcPaths: string[], normalizePath = true) {
1✔
747
        for (let srcPath of srcPaths) {
1✔
748
            this.removeFile(srcPath, normalizePath);
1✔
749
        }
750
    }
751

752
    /**
753
     * Remove a file from the program
754
     * @param filePath can be a srcPath, a destPath, or a destPath with leading `pkg:/`
755
     * @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
756
     */
757
    public removeFile(filePath: string, normalizePath = true, keepSymbolInformation = false) {
27✔
758
        this.logger.debug('Program.removeFile()', filePath);
150✔
759
        const paths = this.getPaths(filePath, this.options.rootDir);
150✔
760

761
        //there can be one or more File entries for a single srcPath, so get all of them and remove them all
762
        const files = this.fileClusters.get(paths.srcPath?.toLowerCase()) ?? [this.getFile(filePath, normalizePath)];
150!
763

764
        for (const file of files) {
150✔
765
            //if a file has already been removed, nothing more needs to be done here
766
            if (!file || !this.hasFile(file.srcPath)) {
153✔
767
                continue;
1✔
768
            }
769
            this.diagnostics.clearForFile(file.srcPath);
152✔
770

771
            const event: BeforeFileRemoveEvent = { file: file, program: this };
152✔
772
            this.plugins.emit('beforeFileRemove', event);
152✔
773

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

792
            this.dependencyGraph.remove(file.dependencyGraphKey);
152✔
793

794
            //if this is a pkg:/source file, notify the `source` scope that it has changed
795
            if (this.isSourceBrsFile(file)) {
152✔
796
                this.dependencyGraph.removeDependency('scope:source', file.dependencyGraphKey);
126✔
797
            }
798
            if (isBrsFile(file)) {
152✔
799
                if (!keepSymbolInformation) {
135✔
800
                    this.fileSymbolInformation.delete(file.pkgPath);
8✔
801
                }
802
                this.crossScopeValidation.clearResolutionsForFile(file);
135✔
803
            }
804

805
            //if this is a component, remove it from our components map
806
            if (isXmlFile(file)) {
152✔
807
                this.unregisterComponent(file);
11✔
808
            }
809
            //dispose any disposable things on the file
810
            for (const disposable of file?.disposables ?? []) {
152!
811
                disposable();
146✔
812
            }
813
            //dispose file
814
            file?.dispose?.();
152!
815

816
            this.plugins.emit('afterFileRemove', event);
152✔
817
        }
818
    }
819

820
    public crossScopeValidation = new CrossScopeValidator(this);
1,829✔
821

822
    private isFirstValidation = true;
1,829✔
823

824
    /**
825
     * Traverse the entire project, and validate all scopes
826
     */
827
    public validate() {
828
        this.logger.time(LogLevel.log, ['Validating project'], () => {
1,367✔
829
            this.diagnostics.clearForTag(ProgramValidatorDiagnosticsTag);
1,367✔
830
            const programValidateEvent = {
1,367✔
831
                program: this
832
            };
833
            this.plugins.emit('beforeProgramValidate', programValidateEvent);
1,367✔
834
            this.plugins.emit('onProgramValidate', programValidateEvent);
1,367✔
835

836
            const metrics = {
1,367✔
837
                filesChanged: 0,
838
                filesValidated: 0,
839
                fileValidationTime: '',
840
                crossScopeValidationTime: '',
841
                scopesValidated: 0,
842
                totalLinkTime: '',
843
                totalScopeValidationTime: '',
844
                componentValidationTime: ''
845
            };
846

847
            const validationStopwatch = new Stopwatch();
1,367✔
848
            //validate every file
849
            const brsFilesValidated: BrsFile[] = [];
1,367✔
850
            const afterValidateFiles: BscFile[] = [];
1,367✔
851

852
            metrics.fileValidationTime = validationStopwatch.getDurationTextFor(() => {
1,367✔
853
                //sort files by path so we get consistent results
854
                const files = Object.values(this.files).sort(firstBy(x => x.srcPath));
3,488✔
855
                for (const file of files) {
1,367✔
856
                    //for every unvalidated file, validate it
857
                    if (!file.isValidated) {
2,259✔
858
                        const validateFileEvent = {
1,927✔
859
                            program: this,
860
                            file: file
861
                        };
862
                        this.plugins.emit('beforeFileValidate', validateFileEvent);
1,927✔
863
                        //emit an event to allow plugins to contribute to the file validation process
864
                        this.plugins.emit('onFileValidate', validateFileEvent);
1,927✔
865
                        file.isValidated = true;
1,927✔
866
                        if (isBrsFile(file)) {
1,927✔
867
                            brsFilesValidated.push(file);
1,622✔
868
                        }
869
                        afterValidateFiles.push(file);
1,927✔
870
                    }
871
                }
872
                // AfterFileValidate is after all files have been validated
873
                for (const file of afterValidateFiles) {
1,367✔
874
                    const validateFileEvent = {
1,927✔
875
                        program: this,
876
                        file: file
877
                    };
878
                    this.plugins.emit('afterFileValidate', validateFileEvent);
1,927✔
879
                }
880
            }).durationText;
881

882
            metrics.filesChanged = afterValidateFiles.length;
1,367✔
883

884
            // Build component types for any component that changes
885
            this.logger.time(LogLevel.info, ['Build component types'], () => {
1,367✔
886
                for (let { componentKey, componentName } of this.componentSymbolsToUpdate) {
1,367✔
887
                    this.updateComponentSymbolInGlobalScope(componentKey, componentName);
308✔
888
                }
889
                this.componentSymbolsToUpdate.clear();
1,367✔
890
            });
891

892

893
            const changedSymbolsMapArr = brsFilesValidated?.map(f => {
1,367!
894
                if (isBrsFile(f)) {
1,622!
895
                    return f.providedSymbols.changes;
1,622✔
896
                }
UNCOV
897
                return null;
×
898
            }).filter(x => x);
1,622✔
899

900
            const changedSymbols = new Map<SymbolTypeFlag, Set<string>>();
1,367✔
901
            for (const flag of [SymbolTypeFlag.runtime, SymbolTypeFlag.typetime]) {
1,367✔
902
                const changedSymbolsSetArr = changedSymbolsMapArr.map(symMap => symMap.get(flag));
3,244✔
903
                changedSymbols.set(flag, new Set(...changedSymbolsSetArr));
2,734✔
904
            }
905

906
            const filesToBeValidatedInScopeContext = new Set<BscFile>(afterValidateFiles);
1,367✔
907

908
            metrics.crossScopeValidationTime = validationStopwatch.getDurationTextFor(() => {
1,367✔
909
                const scopesToCheck = this.getScopesForCrossScopeValidation();
1,367✔
910
                this.crossScopeValidation.buildComponentsMap();
1,367✔
911
                this.crossScopeValidation.addDiagnosticsForScopes(scopesToCheck);
1,367✔
912
                const filesToRevalidate = this.crossScopeValidation.getFilesRequiringChangedSymbol(scopesToCheck, changedSymbols);
1,367✔
913
                for (const file of filesToRevalidate) {
1,367✔
914
                    filesToBeValidatedInScopeContext.add(file);
178✔
915
                }
916
            }).durationText;
917

918
            metrics.filesValidated = filesToBeValidatedInScopeContext.size;
1,367✔
919

920
            let linkTime = 0;
1,367✔
921
            let validationTime = 0;
1,367✔
922
            let scopesValidated = 0;
1,367✔
923
            let changedFiles = new Set<BscFile>(afterValidateFiles);
1,367✔
924
            this.logger.time(LogLevel.info, ['Validate all scopes'], () => {
1,367✔
925
                //sort the scope names so we get consistent results
926
                const scopeNames = this.getSortedScopeNames();
1,367✔
927
                for (const file of filesToBeValidatedInScopeContext) {
1,367✔
928
                    if (isBrsFile(file)) {
2,048✔
929
                        file.validationSegmenter.unValidateAllSegments();
1,743✔
930
                    }
931
                }
932
                for (let scopeName of scopeNames) {
1,367✔
933
                    let scope = this.scopes[scopeName];
3,084✔
934
                    const scopeValidated = scope.validate({
3,084✔
935
                        filesToBeValidatedInScopeContext: filesToBeValidatedInScopeContext,
936
                        changedSymbols: changedSymbols,
937
                        changedFiles: changedFiles,
938
                        initialValidation: this.isFirstValidation
939
                    });
940
                    if (scopeValidated) {
3,084✔
941
                        scopesValidated++;
1,667✔
942
                    }
943
                    linkTime += scope.validationMetrics.linkTime;
3,084✔
944
                    validationTime += scope.validationMetrics.validationTime;
3,084✔
945
                }
946
            });
947
            metrics.scopesValidated = scopesValidated;
1,367✔
948
            validationStopwatch.totalMilliseconds = linkTime;
1,367✔
949
            metrics.totalLinkTime = validationStopwatch.getDurationText();
1,367✔
950

951
            validationStopwatch.totalMilliseconds = validationTime;
1,367✔
952
            metrics.totalScopeValidationTime = validationStopwatch.getDurationText();
1,367✔
953

954
            metrics.componentValidationTime = validationStopwatch.getDurationTextFor(() => {
1,367✔
955
                this.detectDuplicateComponentNames();
1,367✔
956
            }).durationText;
957

958
            this.logValidationMetrics(metrics);
1,367✔
959

960
            this.isFirstValidation = false;
1,367✔
961

962
            this.plugins.emit('afterProgramValidate', programValidateEvent);
1,367✔
963
        });
964
    }
965

966
    // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
967
    private logValidationMetrics(metrics: { [key: string]: number | string }) {
968
        let logs = [] as string[];
1,367✔
969
        for (const key in metrics) {
1,367✔
970
            logs.push(`${key}=${chalk.yellow(metrics[key].toString())}`);
10,936✔
971
        }
972
        this.logger.info(`Validation Metrics: ${logs.join(', ')}`);
1,367✔
973
    }
974

975
    private getScopesForCrossScopeValidation() {
976
        const scopesForCrossScopeValidation = [];
1,367✔
977
        for (let scopeName of this.getSortedScopeNames()) {
1,367✔
978
            let scope = this.scopes[scopeName];
3,084✔
979
            if (this.globalScope !== scope && !scope.isValidated) {
3,084✔
980
                scopesForCrossScopeValidation.push(scope);
1,688✔
981
            }
982
        }
983
        return scopesForCrossScopeValidation;
1,367✔
984
    }
985

986
    /**
987
     * Flag all duplicate component names
988
     */
989
    private detectDuplicateComponentNames() {
990
        const componentsByName = Object.keys(this.files).reduce<Record<string, XmlFile[]>>((map, filePath) => {
1,367✔
991
            const file = this.files[filePath];
2,259✔
992
            //if this is an XmlFile, and it has a valid `componentName` property
993
            if (isXmlFile(file) && file.componentName?.text) {
2,259✔
994
                let lowerName = file.componentName.text.toLowerCase();
437✔
995
                if (!map[lowerName]) {
437✔
996
                    map[lowerName] = [];
434✔
997
                }
998
                map[lowerName].push(file);
437✔
999
            }
1000
            return map;
2,259✔
1001
        }, {});
1002

1003
        for (let name in componentsByName) {
1,367✔
1004
            const xmlFiles = componentsByName[name];
434✔
1005
            //add diagnostics for every duplicate component with this name
1006
            if (xmlFiles.length > 1) {
434✔
1007
                for (let xmlFile of xmlFiles) {
3✔
1008
                    const { componentName } = xmlFile;
6✔
1009
                    this.diagnostics.register({
6✔
1010
                        ...DiagnosticMessages.duplicateComponentName(componentName.text),
1011
                        location: xmlFile.componentName.location,
1012
                        relatedInformation: xmlFiles.filter(x => x !== xmlFile).map(x => {
12✔
1013
                            return {
6✔
1014
                                location: x.componentName.location,
1015
                                message: 'Also defined here'
1016
                            };
1017
                        })
1018
                    }, { tags: [ProgramValidatorDiagnosticsTag] });
1019
                }
1020
            }
1021
        }
1022
    }
1023

1024
    /**
1025
     * Get the files for a list of filePaths
1026
     * @param filePaths can be an array of srcPath or a destPath strings
1027
     * @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
1028
     */
1029
    public getFiles<T extends BscFile>(filePaths: string[], normalizePath = true) {
29✔
1030
        return filePaths
29✔
1031
            .map(filePath => this.getFile(filePath, normalizePath))
39✔
1032
            .filter(file => file !== undefined) as T[];
39✔
1033
    }
1034

1035
    /**
1036
     * Get the file at the given path
1037
     * @param filePath can be a srcPath or a destPath
1038
     * @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
1039
     */
1040
    public getFile<T extends BscFile>(filePath: string, normalizePath = true) {
15,121✔
1041
        if (typeof filePath !== 'string') {
19,107✔
1042
            return undefined;
3,387✔
1043
            //is the path absolute (or the `virtual:` prefix)
1044
        } else if (/^(?:(?:virtual:[\/\\])|(?:\w:)|(?:[\/\\]))/gmi.exec(filePath)) {
15,720✔
1045
            return this.files[
4,609✔
1046
                (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase()
4,609!
1047
            ] as T;
1048
        } else if (util.isUriLike(filePath)) {
11,111✔
1049
            const path = URI.parse(filePath).fsPath;
1,332✔
1050
            return this.files[
1,332✔
1051
                (normalizePath ? util.standardizePath(path) : path).toLowerCase()
1,332!
1052
            ] as T;
1053
        } else {
1054
            return this.destMap.get(
9,779✔
1055
                (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase()
9,779✔
1056
            ) as T;
1057
        }
1058
    }
1059

1060
    private sortedScopeNames: string[] = undefined;
1,829✔
1061

1062
    /**
1063
     * Gets a sorted list of all scopeNames, always beginning with "global", "source", then any others in alphabetical order
1064
     */
1065
    private getSortedScopeNames() {
1066
        if (!this.sortedScopeNames) {
7,480✔
1067
            this.sortedScopeNames = Object.keys(this.scopes).sort((a, b) => {
1,322✔
1068
                if (a === 'global') {
1,852!
UNCOV
1069
                    return -1;
×
1070
                } else if (b === 'global') {
1,852✔
1071
                    return 1;
1,302✔
1072
                }
1073
                if (a === 'source') {
550✔
1074
                    return -1;
26✔
1075
                } else if (b === 'source') {
524✔
1076
                    return 1;
108✔
1077
                }
1078
                if (a < b) {
416✔
1079
                    return -1;
166✔
1080
                } else if (b < a) {
250!
1081
                    return 1;
250✔
1082
                }
UNCOV
1083
                return 0;
×
1084
            });
1085
        }
1086
        return this.sortedScopeNames;
7,480✔
1087
    }
1088

1089
    /**
1090
     * Get a list of all scopes the file is loaded into
1091
     * @param file the file
1092
     */
1093
    public getScopesForFile(file: BscFile | string) {
1094
        const resolvedFile = typeof file === 'string' ? this.getFile(file) : file;
717✔
1095

1096
        let result = [] as Scope[];
717✔
1097
        if (resolvedFile) {
717✔
1098
            const scopeKeys = this.getSortedScopeNames();
716✔
1099
            for (let key of scopeKeys) {
716✔
1100
                let scope = this.scopes[key];
1,495✔
1101

1102
                if (scope.hasFile(resolvedFile)) {
1,495✔
1103
                    result.push(scope);
730✔
1104
                }
1105
            }
1106
        }
1107
        return result;
717✔
1108
    }
1109

1110
    /**
1111
     * Get the first found scope for a file.
1112
     */
1113
    public getFirstScopeForFile(file: BscFile): Scope | undefined {
1114
        const scopeKeys = this.getSortedScopeNames();
4,030✔
1115
        for (let key of scopeKeys) {
4,030✔
1116
            let scope = this.scopes[key];
18,403✔
1117

1118
            if (scope.hasFile(file)) {
18,403✔
1119
                return scope;
2,919✔
1120
            }
1121
        }
1122
    }
1123

1124
    public getStatementsByName(name: string, originFile: BrsFile, namespaceName?: string): FileLink<Statement>[] {
1125
        let results = new Map<Statement, FileLink<Statement>>();
39✔
1126
        const filesSearched = new Set<BrsFile>();
39✔
1127
        let lowerNamespaceName = namespaceName?.toLowerCase();
39✔
1128
        let lowerName = name?.toLowerCase();
39!
1129

1130
        function addToResults(statement: FunctionStatement | MethodStatement, file: BrsFile) {
1131
            let parentNamespaceName = statement.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(originFile.parseMode)?.toLowerCase();
98✔
1132
            if (statement.tokens.name.text.toLowerCase() === lowerName && (!lowerNamespaceName || parentNamespaceName === lowerNamespaceName)) {
98✔
1133
                if (!results.has(statement)) {
36!
1134
                    results.set(statement, { item: statement, file: file as BrsFile });
36✔
1135
                }
1136
            }
1137
        }
1138

1139
        //look through all files in scope for matches
1140
        for (const scope of this.getScopesForFile(originFile)) {
39✔
1141
            for (const file of scope.getAllFiles()) {
39✔
1142
                //skip non-brs files, or files we've already processed
1143
                if (!isBrsFile(file) || filesSearched.has(file)) {
45✔
1144
                    continue;
3✔
1145
                }
1146
                filesSearched.add(file);
42✔
1147

1148
                file.ast.walk(createVisitor({
42✔
1149
                    FunctionStatement: (statement: FunctionStatement) => {
1150
                        addToResults(statement, file);
95✔
1151
                    },
1152
                    MethodStatement: (statement: MethodStatement) => {
1153
                        addToResults(statement, file);
3✔
1154
                    }
1155
                }), {
1156
                    walkMode: WalkMode.visitStatements
1157
                });
1158
            }
1159
        }
1160
        return [...results.values()];
39✔
1161
    }
1162

1163
    public getStatementsForXmlFile(scope: XmlScope, filterName?: string): FileLink<FunctionStatement>[] {
1164
        let results = new Map<Statement, FileLink<FunctionStatement>>();
10✔
1165
        const filesSearched = new Set<BrsFile>();
10✔
1166

1167
        //get all function names for the xml file and parents
1168
        let funcNames = new Set<string>();
10✔
1169
        let currentScope = scope;
10✔
1170
        while (isXmlScope(currentScope)) {
10✔
1171
            for (let name of currentScope.xmlFile.ast.componentElement.interfaceElement?.functions.map((f) => f.name) ?? []) {
16✔
1172
                if (!filterName || name === filterName) {
16!
1173
                    funcNames.add(name);
16✔
1174
                }
1175
            }
1176
            currentScope = currentScope.getParentScope() as XmlScope;
12✔
1177
        }
1178

1179
        //look through all files in scope for matches
1180
        for (const file of scope.getOwnFiles()) {
10✔
1181
            //skip non-brs files, or files we've already processed
1182
            if (!isBrsFile(file) || filesSearched.has(file)) {
20✔
1183
                continue;
10✔
1184
            }
1185
            filesSearched.add(file);
10✔
1186

1187
            file.ast.walk(createVisitor({
10✔
1188
                FunctionStatement: (statement: FunctionStatement) => {
1189
                    if (funcNames.has(statement.tokens.name.text)) {
15!
1190
                        if (!results.has(statement)) {
15!
1191
                            results.set(statement, { item: statement, file: file });
15✔
1192
                        }
1193
                    }
1194
                }
1195
            }), {
1196
                walkMode: WalkMode.visitStatements
1197
            });
1198
        }
1199
        return [...results.values()];
10✔
1200
    }
1201

1202
    /**
1203
     * Find all available completion items at the given position
1204
     * @param filePath can be a srcPath or a destPath
1205
     * @param position the position (line & column) where completions should be found
1206
     */
1207
    public getCompletions(filePath: string, position: Position) {
1208
        let file = this.getFile(filePath);
116✔
1209
        if (!file) {
116!
UNCOV
1210
            return [];
×
1211
        }
1212

1213
        //find the scopes for this file
1214
        let scopes = this.getScopesForFile(file);
116✔
1215

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

1219
        const event: ProvideCompletionsEvent = {
116✔
1220
            program: this,
1221
            file: file,
1222
            scopes: scopes,
1223
            position: position,
1224
            completions: []
1225
        };
1226

1227
        this.plugins.emit('beforeProvideCompletions', event);
116✔
1228

1229
        this.plugins.emit('provideCompletions', event);
116✔
1230

1231
        this.plugins.emit('afterProvideCompletions', event);
116✔
1232

1233
        return event.completions;
116✔
1234
    }
1235

1236
    /**
1237
     * Goes through each file and builds a list of workspace symbols for the program. Used by LanguageServer's onWorkspaceSymbol functionality
1238
     */
1239
    public getWorkspaceSymbols() {
1240
        const event: ProvideWorkspaceSymbolsEvent = {
22✔
1241
            program: this,
1242
            workspaceSymbols: []
1243
        };
1244
        this.plugins.emit('beforeProvideWorkspaceSymbols', event);
22✔
1245
        this.plugins.emit('provideWorkspaceSymbols', event);
22✔
1246
        this.plugins.emit('afterProvideWorkspaceSymbols', event);
22✔
1247
        return event.workspaceSymbols;
22✔
1248
    }
1249

1250
    /**
1251
     * Given a position in a file, if the position is sitting on some type of identifier,
1252
     * go to the definition of that identifier (where this thing was first defined)
1253
     */
1254
    public getDefinition(srcPath: string, position: Position): Location[] {
1255
        let file = this.getFile(srcPath);
18✔
1256
        if (!file) {
18!
UNCOV
1257
            return [];
×
1258
        }
1259

1260
        const event: ProvideDefinitionEvent = {
18✔
1261
            program: this,
1262
            file: file,
1263
            position: position,
1264
            definitions: []
1265
        };
1266

1267
        this.plugins.emit('beforeProvideDefinition', event);
18✔
1268
        this.plugins.emit('provideDefinition', event);
18✔
1269
        this.plugins.emit('afterProvideDefinition', event);
18✔
1270
        return event.definitions;
18✔
1271
    }
1272

1273
    /**
1274
     * Get hover information for a file and position
1275
     */
1276
    public getHover(srcPath: string, position: Position): Hover[] {
1277
        let file = this.getFile(srcPath);
68✔
1278
        let result: Hover[];
1279
        if (file) {
68!
1280
            const event = {
68✔
1281
                program: this,
1282
                file: file,
1283
                position: position,
1284
                scopes: this.getScopesForFile(file),
1285
                hovers: []
1286
            } as ProvideHoverEvent;
1287
            this.plugins.emit('beforeProvideHover', event);
68✔
1288
            this.plugins.emit('provideHover', event);
68✔
1289
            this.plugins.emit('afterProvideHover', event);
68✔
1290
            result = event.hovers;
68✔
1291
        }
1292

1293
        return result ?? [];
68!
1294
    }
1295

1296
    /**
1297
     * Get full list of document symbols for a file
1298
     * @param srcPath path to the file
1299
     */
1300
    public getDocumentSymbols(srcPath: string): DocumentSymbol[] | undefined {
1301
        let file = this.getFile(srcPath);
18✔
1302
        if (file) {
18!
1303
            const event: ProvideDocumentSymbolsEvent = {
18✔
1304
                program: this,
1305
                file: file,
1306
                documentSymbols: []
1307
            };
1308
            this.plugins.emit('beforeProvideDocumentSymbols', event);
18✔
1309
            this.plugins.emit('provideDocumentSymbols', event);
18✔
1310
            this.plugins.emit('afterProvideDocumentSymbols', event);
18✔
1311
            return event.documentSymbols;
18✔
1312
        } else {
UNCOV
1313
            return undefined;
×
1314
        }
1315
    }
1316

1317
    /**
1318
     * Compute code actions for the given file and range
1319
     */
1320
    public getCodeActions(srcPath: string, range: Range) {
1321
        const codeActions = [] as CodeAction[];
13✔
1322
        const file = this.getFile(srcPath);
13✔
1323
        if (file) {
13✔
1324
            const fileUri = util.pathToUri(file?.srcPath);
12!
1325
            const diagnostics = this
12✔
1326
                //get all current diagnostics (filtered by diagnostic filters)
1327
                .getDiagnostics()
1328
                //only keep diagnostics related to this file
1329
                .filter(x => x.location?.uri === fileUri)
22✔
1330
                //only keep diagnostics that touch this range
1331
                .filter(x => util.rangesIntersectOrTouch(x.location.range, range));
12✔
1332

1333
            const scopes = this.getScopesForFile(file);
12✔
1334

1335
            this.plugins.emit('onGetCodeActions', {
12✔
1336
                program: this,
1337
                file: file,
1338
                range: range,
1339
                diagnostics: diagnostics,
1340
                scopes: scopes,
1341
                codeActions: codeActions
1342
            });
1343
        }
1344
        return codeActions;
13✔
1345
    }
1346

1347
    /**
1348
     * Get semantic tokens for the specified file
1349
     */
1350
    public getSemanticTokens(srcPath: string): SemanticToken[] | undefined {
1351
        const file = this.getFile(srcPath);
24✔
1352
        if (file) {
24!
1353
            const result = [] as SemanticToken[];
24✔
1354
            this.plugins.emit('onGetSemanticTokens', {
24✔
1355
                program: this,
1356
                file: file,
1357
                scopes: this.getScopesForFile(file),
1358
                semanticTokens: result
1359
            });
1360
            return result;
24✔
1361
        }
1362
    }
1363

1364
    public getSignatureHelp(filepath: string, position: Position): SignatureInfoObj[] {
1365
        let file: BrsFile = this.getFile(filepath);
185✔
1366
        if (!file || !isBrsFile(file)) {
185✔
1367
            return [];
3✔
1368
        }
1369
        let callExpressionInfo = new CallExpressionInfo(file, position);
182✔
1370
        let signatureHelpUtil = new SignatureHelpUtil();
182✔
1371
        return signatureHelpUtil.getSignatureHelpItems(callExpressionInfo);
182✔
1372
    }
1373

1374
    public getReferences(srcPath: string, position: Position): Location[] {
1375
        //find the file
1376
        let file = this.getFile(srcPath);
4✔
1377

1378
        const event: ProvideReferencesEvent = {
4✔
1379
            program: this,
1380
            file: file,
1381
            position: position,
1382
            references: []
1383
        };
1384

1385
        this.plugins.emit('beforeProvideReferences', event);
4✔
1386
        this.plugins.emit('provideReferences', event);
4✔
1387
        this.plugins.emit('afterProvideReferences', event);
4✔
1388

1389
        return event.references;
4✔
1390
    }
1391

1392
    /**
1393
     * Transpile a single file and get the result as a string.
1394
     * This does not write anything to the file system.
1395
     *
1396
     * This should only be called by `LanguageServer`.
1397
     * Internal usage should call `_getTranspiledFileContents` instead.
1398
     * @param filePath can be a srcPath or a destPath
1399
     */
1400
    public async getTranspiledFileContents(filePath: string): Promise<FileTranspileResult> {
1401
        const file = this.getFile(filePath);
318✔
1402

1403
        return this.getTranspiledFileContentsPipeline.run(async () => {
318✔
1404

1405
            const result = {
318✔
1406
                destPath: file.destPath,
1407
                pkgPath: file.pkgPath,
1408
                srcPath: file.srcPath
1409
            } as FileTranspileResult;
1410

1411
            const expectedPkgPath = file.pkgPath.toLowerCase();
318✔
1412
            const expectedMapPath = `${expectedPkgPath}.map`;
318✔
1413
            const expectedTypedefPkgPath = expectedPkgPath.replace(/\.brs$/i, '.d.bs');
318✔
1414

1415
            //add a temporary plugin to tap into the file writing process
1416
            const plugin = this.plugins.addFirst({
318✔
1417
                name: 'getTranspiledFileContents',
1418
                beforeWriteFile: (event) => {
1419
                    const pkgPath = event.file.pkgPath.toLowerCase();
992✔
1420
                    switch (pkgPath) {
992✔
1421
                        //this is the actual transpiled file
1422
                        case expectedPkgPath:
992✔
1423
                            result.code = event.file.data.toString();
318✔
1424
                            break;
318✔
1425
                        //this is the sourcemap
1426
                        case expectedMapPath:
1427
                            result.map = event.file.data.toString();
170✔
1428
                            break;
170✔
1429
                        //this is the typedef
1430
                        case expectedTypedefPkgPath:
1431
                            result.typedef = event.file.data.toString();
8✔
1432
                            break;
8✔
1433
                        default:
1434
                        //no idea what this file is. just ignore it
1435
                    }
1436
                    //mark every file as processed so it they don't get written to the output directory
1437
                    event.processedFiles.add(event.file);
992✔
1438
                }
1439
            });
1440

1441
            try {
318✔
1442
                //now that the plugin has been registered, run the build with just this file
1443
                await this.build({
318✔
1444
                    files: [file]
1445
                });
1446
            } finally {
1447
                this.plugins.remove(plugin);
318✔
1448
            }
1449
            return result;
318✔
1450
        });
1451
    }
1452
    private getTranspiledFileContentsPipeline = new ActionPipeline();
1,829✔
1453

1454
    /**
1455
     * Get the absolute output path for a file
1456
     */
1457
    private getOutputPath(file: { pkgPath?: string }, stagingDir = this.getStagingDir()) {
×
1458
        return s`${stagingDir}/${file.pkgPath}`;
1,831✔
1459
    }
1460

1461
    private getStagingDir(stagingDir?: string) {
1462
        let result = stagingDir ?? this.options.stagingDir ?? this.options.stagingDir;
717✔
1463
        if (!result) {
717✔
1464
            result = rokuDeploy.getOptions(this.options as any).stagingDir;
531✔
1465
        }
1466
        result = s`${path.resolve(this.options.cwd ?? process.cwd(), result ?? '/')}`;
717!
1467
        return result;
717✔
1468
    }
1469

1470
    /**
1471
     * Prepare the program for building
1472
     * @param files the list of files that should be prepared
1473
     */
1474
    private async prepare(files: BscFile[]) {
1475
        const programEvent: PrepareProgramEvent = {
359✔
1476
            program: this,
1477
            editor: this.editor,
1478
            files: files
1479
        };
1480

1481
        //assign an editor to every file
1482
        for (const file of programEvent.files) {
359✔
1483
            //if the file doesn't have an editor yet, assign one now
1484
            if (!file.editor) {
728✔
1485
                file.editor = new Editor();
681✔
1486
            }
1487
        }
1488

1489
        //sort the entries to make transpiling more deterministic
1490
        programEvent.files.sort((a, b) => {
359✔
1491
            if (a.pkgPath < b.pkgPath) {
384✔
1492
                return -1;
324✔
1493
            } else if (a.pkgPath > b.pkgPath) {
60!
1494
                return 1;
60✔
1495
            } else {
UNCOV
1496
                return 1;
×
1497
            }
1498
        });
1499

1500
        await this.plugins.emitAsync('beforePrepareProgram', programEvent);
359✔
1501
        await this.plugins.emitAsync('prepareProgram', programEvent);
359✔
1502

1503
        const stagingDir = this.getStagingDir();
359✔
1504

1505
        const entries: TranspileObj[] = [];
359✔
1506

1507
        for (const file of files) {
359✔
1508
            const scope = this.getFirstScopeForFile(file);
728✔
1509
            //link the symbol table for all the files in this scope
1510
            scope?.linkSymbolTable();
728✔
1511

1512
            //if the file doesn't have an editor yet, assign one now
1513
            if (!file.editor) {
728!
UNCOV
1514
                file.editor = new Editor();
×
1515
            }
1516
            const event = {
728✔
1517
                program: this,
1518
                file: file,
1519
                editor: file.editor,
1520
                scope: scope,
1521
                outputPath: this.getOutputPath(file, stagingDir)
1522
            } as PrepareFileEvent & { outputPath: string };
1523

1524
            await this.plugins.emitAsync('beforePrepareFile', event);
728✔
1525
            await this.plugins.emitAsync('prepareFile', event);
728✔
1526
            await this.plugins.emitAsync('afterPrepareFile', event);
728✔
1527

1528
            //TODO remove this in v1
1529
            entries.push(event);
728✔
1530

1531
            //unlink the symbolTable so the next loop iteration can link theirs
1532
            scope?.unlinkSymbolTable();
728✔
1533
        }
1534

1535
        await this.plugins.emitAsync('afterPrepareProgram', programEvent);
359✔
1536
        return files;
359✔
1537
    }
1538

1539
    /**
1540
     * Generate the contents of every file
1541
     */
1542
    private async serialize(files: BscFile[]) {
1543

1544
        const allFiles = new Map<BscFile, SerializedFile[]>();
358✔
1545

1546
        //exclude prunable files if that option is enabled
1547
        if (this.options.pruneEmptyCodeFiles === true) {
358✔
1548
            files = files.filter(x => x.canBePruned !== true);
9✔
1549
        }
1550

1551
        const serializeProgramEvent = await this.plugins.emitAsync('beforeSerializeProgram', {
358✔
1552
            program: this,
1553
            files: files,
1554
            result: allFiles
1555
        });
1556
        await this.plugins.emitAsync('onSerializeProgram', serializeProgramEvent);
358✔
1557

1558
        // serialize each file
1559
        for (const file of files) {
358✔
1560
            let scope = this.getFirstScopeForFile(file);
725✔
1561

1562
            //if the file doesn't have a scope, create a temporary scope for the file so it can depend on scope-level items
1563
            if (!scope) {
725✔
1564
                scope = new Scope(`temporary-for-${file.pkgPath}`, this);
369✔
1565
                scope.getAllFiles = () => [file];
3,308✔
1566
                scope.getOwnFiles = scope.getAllFiles;
369✔
1567
            }
1568

1569
            //link the symbol table for all the files in this scope
1570
            scope?.linkSymbolTable();
725!
1571
            const event: SerializeFileEvent = {
725✔
1572
                program: this,
1573
                file: file,
1574
                scope: scope,
1575
                result: allFiles
1576
            };
1577
            await this.plugins.emitAsync('beforeSerializeFile', event);
725✔
1578
            await this.plugins.emitAsync('serializeFile', event);
725✔
1579
            await this.plugins.emitAsync('afterSerializeFile', event);
725✔
1580
            //unlink the symbolTable so the next loop iteration can link theirs
1581
            scope?.unlinkSymbolTable();
725!
1582
        }
1583

1584
        this.plugins.emit('afterSerializeProgram', serializeProgramEvent);
358✔
1585

1586
        return allFiles;
358✔
1587
    }
1588

1589
    /**
1590
     * Write the entire project to disk
1591
     */
1592
    private async write(stagingDir: string, files: Map<BscFile, SerializedFile[]>) {
1593
        const programEvent = await this.plugins.emitAsync('beforeWriteProgram', {
358✔
1594
            program: this,
1595
            files: files,
1596
            stagingDir: stagingDir
1597
        });
1598
        //empty the staging directory
1599
        await fsExtra.emptyDir(stagingDir);
358✔
1600

1601
        const serializedFiles = [...files]
358✔
1602
            .map(([, serializedFiles]) => serializedFiles)
725✔
1603
            .flat();
1604

1605
        //write all the files to disk (asynchronously)
1606
        await Promise.all(
358✔
1607
            serializedFiles.map(async (file) => {
1608
                const event = await this.plugins.emitAsync('beforeWriteFile', {
1,103✔
1609
                    program: this,
1610
                    file: file,
1611
                    outputPath: this.getOutputPath(file, stagingDir),
1612
                    processedFiles: new Set<SerializedFile>()
1613
                });
1614

1615
                await this.plugins.emitAsync('writeFile', event);
1,103✔
1616

1617
                await this.plugins.emitAsync('afterWriteFile', event);
1,103✔
1618
            })
1619
        );
1620

1621
        await this.plugins.emitAsync('afterWriteProgram', programEvent);
358✔
1622
    }
1623

1624
    private buildPipeline = new ActionPipeline();
1,829✔
1625

1626
    /**
1627
     * Build the project. This transpiles/transforms/copies all files and moves them to the staging directory
1628
     * @param options the list of options used to build the program
1629
     */
1630
    public async build(options?: ProgramBuildOptions) {
1631
        //run a single build at a time
1632
        await this.buildPipeline.run(async () => {
358✔
1633
            const stagingDir = this.getStagingDir(options?.stagingDir);
358✔
1634

1635
            const event = await this.plugins.emitAsync('beforeBuildProgram', {
358✔
1636
                program: this,
1637
                editor: this.editor,
1638
                files: options?.files ?? Object.values(this.files)
2,148✔
1639
            });
1640

1641
            //prepare the program (and files) for building
1642
            event.files = await this.prepare(event.files);
358✔
1643

1644
            //stage the entire program
1645
            const serializedFilesByFile = await this.serialize(event.files);
358✔
1646

1647
            await this.write(stagingDir, serializedFilesByFile);
358✔
1648

1649
            await this.plugins.emitAsync('afterBuildProgram', event);
358✔
1650

1651
            //undo all edits for the program
1652
            this.editor.undoAll();
358✔
1653
            //undo all edits for each file
1654
            for (const file of event.files) {
358✔
1655
                file.editor.undoAll();
726✔
1656
            }
1657
        });
1658
    }
1659

1660
    /**
1661
     * Find a list of files in the program that have a function with the given name (case INsensitive)
1662
     */
1663
    public findFilesForFunction(functionName: string) {
1664
        const files = [] as BscFile[];
7✔
1665
        const lowerFunctionName = functionName.toLowerCase();
7✔
1666
        //find every file with this function defined
1667
        for (const file of Object.values(this.files)) {
7✔
1668
            if (isBrsFile(file)) {
25✔
1669
                //TODO handle namespace-relative function calls
1670
                //if the file has a function with this name
1671
                // eslint-disable-next-line @typescript-eslint/dot-notation
1672
                if (file['_cachedLookups'].functionStatementMap.get(lowerFunctionName)) {
17✔
1673
                    files.push(file);
2✔
1674
                }
1675
            }
1676
        }
1677
        return files;
7✔
1678
    }
1679

1680
    /**
1681
     * Find a list of files in the program that have a class with the given name (case INsensitive)
1682
     */
1683
    public findFilesForClass(className: string) {
1684
        const files = [] as BscFile[];
7✔
1685
        const lowerClassName = className.toLowerCase();
7✔
1686
        //find every file with this class defined
1687
        for (const file of Object.values(this.files)) {
7✔
1688
            if (isBrsFile(file)) {
25✔
1689
                //TODO handle namespace-relative classes
1690
                //if the file has a function with this name
1691

1692
                // eslint-disable-next-line @typescript-eslint/dot-notation
1693
                if (file['_cachedLookups'].classStatementMap.get(lowerClassName) !== undefined) {
17✔
1694
                    files.push(file);
1✔
1695
                }
1696
            }
1697
        }
1698
        return files;
7✔
1699
    }
1700

1701
    public findFilesForNamespace(name: string) {
1702
        const files = [] as BscFile[];
7✔
1703
        const lowerName = name.toLowerCase();
7✔
1704
        //find every file with this class defined
1705
        for (const file of Object.values(this.files)) {
7✔
1706
            if (isBrsFile(file)) {
25✔
1707

1708
                // eslint-disable-next-line @typescript-eslint/dot-notation
1709
                if (file['_cachedLookups'].namespaceStatements.find((x) => {
17✔
1710
                    const namespaceName = x.name.toLowerCase();
7✔
1711
                    return (
7✔
1712
                        //the namespace name matches exactly
1713
                        namespaceName === lowerName ||
9✔
1714
                        //the full namespace starts with the name (honoring the part boundary)
1715
                        namespaceName.startsWith(lowerName + '.')
1716
                    );
1717
                })) {
1718
                    files.push(file);
6✔
1719
                }
1720
            }
1721
        }
1722

1723
        return files;
7✔
1724
    }
1725

1726
    public findFilesForEnum(name: string) {
1727
        const files = [] as BscFile[];
8✔
1728
        const lowerName = name.toLowerCase();
8✔
1729
        //find every file with this enum defined
1730
        for (const file of Object.values(this.files)) {
8✔
1731
            if (isBrsFile(file)) {
26✔
1732
                // eslint-disable-next-line @typescript-eslint/dot-notation
1733
                if (file['_cachedLookups'].enumStatementMap.get(lowerName)) {
18✔
1734
                    files.push(file);
1✔
1735
                }
1736
            }
1737
        }
1738
        return files;
8✔
1739
    }
1740

1741
    private _manifest: Map<string, string>;
1742

1743
    /**
1744
     * Modify a parsed manifest map by reading `bs_const` and injecting values from `options.manifest.bs_const`
1745
     * @param parsedManifest The manifest map to read from and modify
1746
     */
1747
    private buildBsConstsIntoParsedManifest(parsedManifest: Map<string, string>) {
1748
        // Lift the bs_consts defined in the manifest
1749
        let bsConsts = getBsConst(parsedManifest, false);
15✔
1750

1751
        // Override or delete any bs_consts defined in the bs config
1752
        for (const key in this.options?.manifest?.bs_const) {
15!
1753
            const value = this.options.manifest.bs_const[key];
3✔
1754
            if (value === null) {
3✔
1755
                bsConsts.delete(key);
1✔
1756
            } else {
1757
                bsConsts.set(key, value);
2✔
1758
            }
1759
        }
1760

1761
        // convert the new list of bs consts back into a string for the rest of the down stream systems to use
1762
        let constString = '';
15✔
1763
        for (const [key, value] of bsConsts) {
15✔
1764
            constString += `${constString !== '' ? ';' : ''}${key}=${value.toString()}`;
8✔
1765
        }
1766

1767
        // Set the updated bs_const value
1768
        parsedManifest.set('bs_const', constString);
15✔
1769
    }
1770

1771
    /**
1772
     * Try to find and load the manifest into memory
1773
     * @param manifestFileObj A pointer to a potential manifest file object found during loading
1774
     * @param replaceIfAlreadyLoaded should we overwrite the internal `_manifest` if it already exists
1775
     */
1776
    public loadManifest(manifestFileObj?: FileObj, replaceIfAlreadyLoaded = true) {
1,501✔
1777
        //if we already have a manifest instance, and should not replace...then don't replace
1778
        if (!replaceIfAlreadyLoaded && this._manifest) {
1,507!
UNCOV
1779
            return;
×
1780
        }
1781
        let manifestPath = manifestFileObj
1,507✔
1782
            ? manifestFileObj.src
1,507✔
1783
            : path.join(this.options.rootDir, 'manifest');
1784

1785
        try {
1,507✔
1786
            // we only load this manifest once, so do it sync to improve speed downstream
1787
            const contents = fsExtra.readFileSync(manifestPath, 'utf-8');
1,507✔
1788
            const parsedManifest = parseManifest(contents);
15✔
1789
            this.buildBsConstsIntoParsedManifest(parsedManifest);
15✔
1790
            this._manifest = parsedManifest;
15✔
1791
        } catch (e) {
1792
            this._manifest = new Map();
1,492✔
1793
        }
1794
    }
1795

1796
    /**
1797
     * Get a map of the manifest information
1798
     */
1799
    public getManifest() {
1800
        if (!this._manifest) {
2,326✔
1801
            this.loadManifest();
1,500✔
1802
        }
1803
        return this._manifest;
2,326✔
1804
    }
1805

1806
    public dispose() {
1807
        this.plugins.emit('beforeProgramDispose', { program: this });
1,679✔
1808

1809
        for (let filePath in this.files) {
1,679✔
1810
            this.files[filePath]?.dispose?.();
2,045!
1811
        }
1812
        for (let name in this.scopes) {
1,679✔
1813
            this.scopes[name]?.dispose?.();
3,518!
1814
        }
1815
        this.globalScope?.dispose?.();
1,679!
1816
        this.dependencyGraph?.dispose?.();
1,679!
1817
    }
1818
}
1819

1820
export interface FileTranspileResult {
1821
    srcPath: string;
1822
    destPath: string;
1823
    pkgPath: string;
1824
    code: string;
1825
    map: string;
1826
    typedef: string;
1827
}
1828

1829

1830
class ProvideFileEventInternal<TFile extends BscFile = BscFile> implements ProvideFileEvent<TFile> {
1831
    constructor(
1832
        public program: Program,
2,365✔
1833
        public srcPath: string,
2,365✔
1834
        public destPath: string,
2,365✔
1835
        public data: LazyFileData,
2,365✔
1836
        public fileFactory: FileFactory
2,365✔
1837
    ) {
1838
        this.srcExtension = path.extname(srcPath)?.toLowerCase();
2,365!
1839
    }
1840

1841
    public srcExtension: string;
1842

1843
    public files: TFile[] = [];
2,365✔
1844
}
1845

1846
export interface ProgramBuildOptions {
1847
    /**
1848
     * The directory where the final built files should be placed. This directory will be cleared before running
1849
     */
1850
    stagingDir?: string;
1851
    /**
1852
     * An array of files to build. If omitted, the entire list of files from the program will be used instead.
1853
     * Typically you will want to leave this blank
1854
     */
1855
    files?: BscFile[];
1856
}
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