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

rokucommunity / brighterscript / #13233

24 Oct 2024 01:02PM UTC coverage: 86.866% (-1.3%) from 88.214%
#13233

push

web-flow
Merge cc3491b40 into 7cfaaa047

11613 of 14131 branches covered (82.18%)

Branch coverage included in aggregate %.

7028 of 7618 new or added lines in 100 files covered. (92.26%)

87 existing lines in 18 files now uncovered.

12732 of 13895 relevant lines covered (91.63%)

30018.46 hits per line

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

93.31
/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 } 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,772✔
78
        this.logger = logger ?? createLogger(options);
1,772✔
79
        this.plugins = plugins || new PluginInterface([], { logger: this.logger });
1,772✔
80
        this.diagnostics = diagnosticsManager || new DiagnosticManager();
1,772✔
81

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

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

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

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

95
        this.fileFactory = new FileFactory(this);
1,772✔
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,772✔
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,772✔
114
        this.globalScope.attachDependencyGraph(this.dependencyGraph);
1,772✔
115
        this.scopes.global = this.globalScope;
1,772✔
116

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

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

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

128

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

156
        return nodeType;
326,048✔
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
        this.globalScope.symbolTable.addSymbol('boolean', undefined, BooleanType.instance, SymbolTypeFlag.typetime);
1,772✔
165
        this.globalScope.symbolTable.addSymbol('double', undefined, DoubleType.instance, SymbolTypeFlag.typetime);
1,772✔
166
        this.globalScope.symbolTable.addSymbol('dynamic', undefined, DynamicType.instance, SymbolTypeFlag.typetime);
1,772✔
167
        this.globalScope.symbolTable.addSymbol('float', undefined, FloatType.instance, SymbolTypeFlag.typetime);
1,772✔
168
        this.globalScope.symbolTable.addSymbol('function', undefined, new FunctionType(), SymbolTypeFlag.typetime);
1,772✔
169
        this.globalScope.symbolTable.addSymbol('integer', undefined, IntegerType.instance, SymbolTypeFlag.typetime);
1,772✔
170
        this.globalScope.symbolTable.addSymbol('longinteger', undefined, LongIntegerType.instance, SymbolTypeFlag.typetime);
1,772✔
171
        this.globalScope.symbolTable.addSymbol('object', undefined, new ObjectType(), SymbolTypeFlag.typetime);
1,772✔
172
        this.globalScope.symbolTable.addSymbol('string', undefined, StringType.instance, SymbolTypeFlag.typetime);
1,772✔
173
        this.globalScope.symbolTable.addSymbol('void', undefined, VoidType.instance, SymbolTypeFlag.typetime);
1,772✔
174

175
        BuiltInInterfaceAdder.getLookupTable = () => this.globalScope.symbolTable;
697,443✔
176

177
        for (const callable of globalCallables) {
1,772✔
178
            this.globalScope.symbolTable.addSymbol(callable.name, { description: callable.shortDescription }, callable.type, SymbolTypeFlag.runtime);
138,216✔
179
        }
180

181
        for (const ifaceData of Object.values(interfaces) as BRSInterfaceData[]) {
1,772✔
182
            const nodeType = new InterfaceType(ifaceData.name);
155,936✔
183
            nodeType.addBuiltInInterfaces();
155,936✔
184
            this.globalScope.symbolTable.addSymbol(ifaceData.name, { description: ifaceData.description }, nodeType, SymbolTypeFlag.typetime);
155,936✔
185
        }
186

187
        for (const componentData of Object.values(components) as BRSComponentData[]) {
1,772✔
188
            const nodeType = new InterfaceType(componentData.name);
115,180✔
189
            nodeType.addBuiltInInterfaces();
115,180✔
190
            if (componentData.name !== 'roSGNode') {
115,180✔
191
                // we will add `roSGNode` as shorthand for `roSGNodeNode`, since all roSgNode components are SceneGraph nodes
192
                this.globalScope.symbolTable.addSymbol(componentData.name, { description: componentData.description }, nodeType, SymbolTypeFlag.typetime);
113,408✔
193
            }
194
        }
195

196
        for (const nodeData of Object.values(nodes) as SGNodeData[]) {
1,772✔
197
            this.recursivelyAddNodeToSymbolTable(nodeData);
168,340✔
198
        }
199

200
        for (const eventData of Object.values(events) as BRSEventData[]) {
1,772✔
201
            const nodeType = new InterfaceType(eventData.name);
31,896✔
202
            nodeType.addBuiltInInterfaces();
31,896✔
203
            this.globalScope.symbolTable.addSymbol(eventData.name, { description: eventData.description }, nodeType, SymbolTypeFlag.typetime);
31,896✔
204
        }
205

206
    }
207

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

216
    public diagnostics: DiagnosticManager;
217

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

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

229
    private fileSymbolInformation = new Map<string, { provides: ProvidedSymbolInfo; requires: UnresolvedSymbol[] }>();
1,772✔
230

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

238
    public getFileSymbolInfo(file: BrsFile) {
239
        return this.fileSymbolInformation.get(file.pkgPath);
1,634✔
240
    }
241

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

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

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

260
    public get bslibPrefix() {
261
        if (this.bslibPkgPath === bslibNonAliasedRokuModulesPkgPath) {
1,722✔
262
            return 'rokucommunity_bslib';
18✔
263
        } else {
264
            return 'bslib';
1,704✔
265
        }
266
    }
267

268

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

284
    private scopes = {} as Record<string, Scope>;
1,772✔
285

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

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

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

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

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

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

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

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

379
        this.syncComponentDependencyGraph(arr);
11✔
380
        this.addDeferredComponentTypeSymbolCreation(xmlFile);
11✔
381
    }
382

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

390
    }
391

392
    private getComponentKey(xmlFile: XmlFile) {
393
        return (xmlFile.componentName?.text ?? xmlFile.pkgPath).toLowerCase();
780✔
394
    }
395

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

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

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

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

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

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

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

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

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

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

513
        this.plugins.emit('beforeFileAdd', fileAddEvent);
2,318✔
514

515
        this.files[file.srcPath.toLowerCase()] = file;
2,318✔
516
        this.destMap.set(file.destPath.toLowerCase(), file);
2,318✔
517

518
        this.plugins.emit('afterFileAdd', fileAddEvent);
2,318✔
519

520
        return file;
2,318✔
521
    }
522

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

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

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

555
            const data = new LazyFileData(fileData);
2,314✔
556

557
            const event = new ProvideFileEventInternal(this, srcPath, destPath, data, this.fileFactory);
2,314✔
558

559
            this.plugins.emit('beforeProvideFile', event);
2,314✔
560
            this.plugins.emit('provideFile', event);
2,314✔
561
            this.plugins.emit('afterProvideFile', event);
2,314✔
562

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

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

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

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

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

601
                //set the dependencyGraph key for every file to its destPath
602
                file.dependencyGraphKey = file.destPath.toLowerCase();
2,318✔
603

604
                this.assignFile(file);
2,318✔
605

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

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

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

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

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

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

640
            return primaryFile;
2,314✔
641
        });
642
        return file as T;
2,314✔
643
    }
644

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

654
        assert.ok(fileParam, 'fileParam is required');
2,469✔
655

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

664
            if (param.src) {
347✔
665
                srcPath = s`${param.src}`;
346✔
666
            }
667
            if (param.srcPath) {
347!
UNCOV
668
                srcPath = s`${param.srcPath}`;
×
669
            }
670
            if (param.dest) {
347✔
671
                destPath = s`${this.removePkgPrefix(param.dest)}`;
346✔
672
            }
673
            if (param.pkgPath) {
347!
NEW
674
                destPath = s`${this.removePkgPrefix(param.pkgPath)}`;
×
675
            }
676
        }
677

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

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

692
        assert.ok(srcPath, 'fileEntry.src is required');
2,469✔
693
        assert.ok(destPath, 'fileEntry.dest is required');
2,469✔
694

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

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

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

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

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

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

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

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

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

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

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

790
            this.dependencyGraph.remove(file.dependencyGraphKey);
152✔
791

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

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

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

818
    public crossScopeValidation = new CrossScopeValidator(this);
1,772✔
819

820
    private isFirstValidation = true;
1,772✔
821

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

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

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

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

880
            metrics.filesChanged = afterValidateFiles.length;
1,318✔
881

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

890

891
            const changedSymbolsMapArr = brsFilesValidated?.map(f => {
1,318!
892
                if (isBrsFile(f)) {
1,573!
893
                    return f.providedSymbols.changes;
1,573✔
894
                }
NEW
895
                return null;
×
896
            }).filter(x => x);
1,573✔
897

898
            const changedSymbols = new Map<SymbolTypeFlag, Set<string>>();
1,318✔
899
            for (const flag of [SymbolTypeFlag.runtime, SymbolTypeFlag.typetime]) {
1,318✔
900
                const changedSymbolsSetArr = changedSymbolsMapArr.map(symMap => symMap.get(flag));
3,146✔
901
                changedSymbols.set(flag, new Set(...changedSymbolsSetArr));
2,636✔
902
            }
903

904
            const filesToBeValidatedInScopeContext = new Set<BscFile>(afterValidateFiles);
1,318✔
905

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

916
            metrics.filesValidated = filesToBeValidatedInScopeContext.size;
1,318✔
917

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

949
            validationStopwatch.totalMilliseconds = validationTime;
1,318✔
950
            metrics.totalScopeValidationTime = validationStopwatch.getDurationText();
1,318✔
951

952
            metrics.componentValidationTime = validationStopwatch.getDurationTextFor(() => {
1,318✔
953
                this.detectDuplicateComponentNames();
1,318✔
954
            }).durationText;
955

956
            this.logValidationMetrics(metrics);
1,318✔
957

958
            this.isFirstValidation = false;
1,318✔
959

960
            this.plugins.emit('afterProgramValidate', programValidateEvent);
1,318✔
961
        });
962
    }
963

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

973
    private getScopesForCrossScopeValidation() {
974
        const scopesForCrossScopeValidation = [];
1,318✔
975
        for (let scopeName of this.getSortedScopeNames()) {
1,318✔
976
            let scope = this.scopes[scopeName];
2,986✔
977
            if (this.globalScope !== scope && !scope.isValidated) {
2,986✔
978
                scopesForCrossScopeValidation.push(scope);
1,639✔
979
            }
980
        }
981
        return scopesForCrossScopeValidation;
1,318✔
982
    }
983

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

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

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

1033
    /**
1034
     * Get the file at the given path
1035
     * @param filePath can be a srcPath or a destPath
1036
     * @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
1037
     */
1038
    public getFile<T extends BscFile>(filePath: string, normalizePath = true) {
14,066✔
1039
        if (typeof filePath !== 'string') {
18,004✔
1040
            return undefined;
3,295✔
1041
            //is the path absolute (or the `virtual:` prefix)
1042
        } else if (/^(?:(?:virtual:[\/\\])|(?:\w:)|(?:[\/\\]))/gmi.exec(filePath)) {
14,709✔
1043
            return this.files[
4,508✔
1044
                (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase()
4,508!
1045
            ] as T;
1046
        } else if (util.isUriLike(filePath)) {
10,201✔
1047
            const path = URI.parse(filePath).fsPath;
680✔
1048
            return this.files[
680✔
1049
                (normalizePath ? util.standardizePath(path) : path).toLowerCase()
680!
1050
            ] as T;
1051
        } else {
1052
            return this.destMap.get(
9,521✔
1053
                (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase()
9,521✔
1054
            ) as T;
1055
        }
1056
    }
1057

1058
    private sortedScopeNames: string[] = undefined;
1,772✔
1059

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

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

1094
        let result = [] as Scope[];
716✔
1095
        if (resolvedFile) {
716✔
1096
            const scopeKeys = this.getSortedScopeNames();
715✔
1097
            for (let key of scopeKeys) {
715✔
1098
                let scope = this.scopes[key];
1,493✔
1099

1100
                if (scope.hasFile(resolvedFile)) {
1,493✔
1101
                    result.push(scope);
729✔
1102
                }
1103
            }
1104
        }
1105
        return result;
716✔
1106
    }
1107

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

1116
            if (scope.hasFile(file)) {
18,165✔
1117
                return scope;
2,845✔
1118
            }
1119
        }
1120
    }
1121

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

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

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

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

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

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

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

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

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

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

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

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

1225
        this.plugins.emit('beforeProvideCompletions', event);
116✔
1226

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

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

1231
        return event.completions;
116✔
1232
    }
1233

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

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

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

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

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

1291
        return result ?? [];
67!
1292
    }
1293

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

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

1331
            const scopes = this.getScopesForFile(file);
12✔
1332

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

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

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

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

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

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

1387
        return event.references;
4✔
1388
    }
1389

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

1401
        return this.getTranspiledFileContentsPipeline.run(async () => {
303✔
1402

1403
            const result = {
303✔
1404
                destPath: file.destPath,
1405
                pkgPath: file.pkgPath,
1406
                srcPath: file.srcPath
1407
            } as FileTranspileResult;
1408

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

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

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

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

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

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

1479
        //assign an editor to every file
1480
        for (const file of programEvent.files) {
344✔
1481
            //if the file doesn't have an editor yet, assign one now
1482
            if (!file.editor) {
698✔
1483
                file.editor = new Editor();
651✔
1484
            }
1485
        }
1486

1487
        //sort the entries to make transpiling more deterministic
1488
        programEvent.files.sort((a, b) => {
344✔
1489
            if (a.pkgPath < b.pkgPath) {
370✔
1490
                return -1;
309✔
1491
            } else if (a.pkgPath > b.pkgPath) {
61!
1492
                return 1;
61✔
1493
            } else {
NEW
1494
                return 1;
×
1495
            }
1496
        });
1497

1498
        await this.plugins.emitAsync('beforePrepareProgram', programEvent);
344✔
1499
        await this.plugins.emitAsync('prepareProgram', programEvent);
344✔
1500

1501
        const stagingDir = this.getStagingDir();
344✔
1502

1503
        const entries: TranspileObj[] = [];
344✔
1504

1505
        for (const file of files) {
344✔
1506
            const scope = this.getFirstScopeForFile(file);
698✔
1507
            //link the symbol table for all the files in this scope
1508
            scope?.linkSymbolTable();
698✔
1509

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

1522
            await this.plugins.emitAsync('beforePrepareFile', event);
698✔
1523
            await this.plugins.emitAsync('prepareFile', event);
698✔
1524
            await this.plugins.emitAsync('afterPrepareFile', event);
698✔
1525

1526
            //TODO remove this in v1
1527
            entries.push(event);
698✔
1528

1529
            //unlink the symbolTable so the next loop iteration can link theirs
1530
            scope?.unlinkSymbolTable();
698✔
1531
        }
1532

1533
        await this.plugins.emitAsync('afterPrepareProgram', programEvent);
344✔
1534
        return files;
344✔
1535
    }
1536

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

1542
        const allFiles = new Map<BscFile, SerializedFile[]>();
343✔
1543

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

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

1556
        // serialize each file
1557
        for (const file of files) {
343✔
1558
            let scope = this.getFirstScopeForFile(file);
695✔
1559

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

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

1582
        this.plugins.emit('afterSerializeProgram', serializeProgramEvent);
343✔
1583

1584
        return allFiles;
343✔
1585
    }
1586

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

1599
        const serializedFiles = [...files]
343✔
1600
            .map(([, serializedFiles]) => serializedFiles)
695✔
1601
            .flat();
1602

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

1613
                await this.plugins.emitAsync('writeFile', event);
1,067✔
1614

1615
                await this.plugins.emitAsync('afterWriteFile', event);
1,067✔
1616
            })
1617
        );
1618

1619
        await this.plugins.emitAsync('afterWriteProgram', programEvent);
343✔
1620
    }
1621

1622
    private buildPipeline = new ActionPipeline();
1,772✔
1623

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

1633
            const event = await this.plugins.emitAsync('beforeBuildProgram', {
343✔
1634
                program: this,
1635
                editor: this.editor,
1636
                files: options?.files ?? Object.values(this.files)
2,058✔
1637
            });
1638

1639
            //prepare the program (and files) for building
1640
            event.files = await this.prepare(event.files);
343✔
1641

1642
            //stage the entire program
1643
            const serializedFilesByFile = await this.serialize(event.files);
343✔
1644

1645
            await this.write(stagingDir, serializedFilesByFile);
343✔
1646

1647
            await this.plugins.emitAsync('afterBuildProgram', event);
343✔
1648

1649
            //undo all edits for the program
1650
            this.editor.undoAll();
343✔
1651
            //undo all edits for each file
1652
            for (const file of event.files) {
343✔
1653
                file.editor.undoAll();
696✔
1654
            }
1655
        });
1656
    }
1657

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

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

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

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

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

1721
        return files;
7✔
1722
    }
1723

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

1739
    private _manifest: Map<string, string>;
1740

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

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

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

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

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

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

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

1804
    public dispose() {
1805
        this.plugins.emit('beforeProgramDispose', { program: this });
1,624✔
1806

1807
        for (let filePath in this.files) {
1,624✔
1808
            this.files[filePath]?.dispose?.();
1,999!
1809
        }
1810
        for (let name in this.scopes) {
1,624✔
1811
            this.scopes[name]?.dispose?.();
3,416!
1812
        }
1813
        this.globalScope?.dispose?.();
1,624!
1814
        this.dependencyGraph?.dispose?.();
1,624!
1815
    }
1816
}
1817

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

1827

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

1839
    public srcExtension: string;
1840

1841
    public files: TFile[] = [];
2,314✔
1842
}
1843

1844
export interface ProgramBuildOptions {
1845
    /**
1846
     * The directory where the final built files should be placed. This directory will be cleared before running
1847
     */
1848
    stagingDir?: string;
1849
    /**
1850
     * An array of files to build. If omitted, the entire list of files from the program will be used instead.
1851
     * Typically you will want to leave this blank
1852
     */
1853
    files?: BscFile[];
1854
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc