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

rokucommunity / brighterscript / #13307

22 Nov 2024 02:25PM UTC coverage: 86.801%. Remained the same
#13307

push

web-flow
Merge 332332a1f into 2a6afd921

11833 of 14419 branches covered (82.07%)

Branch coverage included in aggregate %.

191 of 205 new or added lines in 26 files covered. (93.17%)

201 existing lines in 18 files now uncovered.

12868 of 14038 relevant lines covered (91.67%)

32019.9 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 } 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
import { SymbolTable } from './SymbolTable';
1✔
58
import { ReferenceType } from './types';
1✔
59

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

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

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

84
        // initialize the diagnostics Manager
85
        this.diagnostics.logger = this.logger;
1,790✔
86
        this.diagnostics.options = this.options;
1,790✔
87
        this.diagnostics.program = this;
1,790✔
88

89
        //inject the bsc plugin as the first plugin in the stack.
90
        this.plugins.addFirst(new BscPlugin());
1,790✔
91

92
        //normalize the root dir path
93
        this.options.rootDir = util.getRootDir(this.options);
1,790✔
94

95
        this.createGlobalScope();
1,790✔
96

97
        this.fileFactory = new FileFactory(this);
1,790✔
98
    }
99

100
    public options: FinalizedBsConfig;
101
    public logger: Logger;
102

103
    /**
104
     * 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`)
105
     */
106
    public editor = new Editor();
1,790✔
107

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

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

119
        this.populateGlobalSymbolTable();
1,790✔
120
        this.globalScope.symbolTable.addSibling(this.componentsTable);
1,790✔
121

122
        //hardcode the files list for global scope to only contain the global file
123
        this.globalScope.getAllFiles = () => [globalFile];
15,322✔
124
        globalFile.isValidated = true;
1,790✔
125
        this.globalScope.validate();
1,790✔
126

127
        //TODO we might need to fix this because the isValidated clears stuff now
128
        (this.globalScope as any).isValidated = true;
1,790✔
129
    }
130

131

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

160
        return nodeType;
331,150✔
161
    }
162
    /**
163
     * Do all setup required for the global symbol table.
164
     */
165
    private populateGlobalSymbolTable() {
166
        //Setup primitive types in global symbolTable
167

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

179
        BuiltInInterfaceAdder.getLookupTable = () => this.globalScope.symbolTable;
786,915✔
180

181
        for (const callable of globalCallables) {
1,790✔
182
            this.globalScope.symbolTable.addSymbol(callable.name, { description: callable.shortDescription }, callable.type, SymbolTypeFlag.runtime);
139,620✔
183
        }
184

185
        for (const ifaceData of Object.values(interfaces) as BRSInterfaceData[]) {
1,790✔
186
            const nodeType = new InterfaceType(ifaceData.name);
157,520✔
187
            nodeType.addBuiltInInterfaces();
157,520✔
188
            nodeType.isBuiltIn = true;
157,520✔
189
            this.globalScope.symbolTable.addSymbol(ifaceData.name, { description: ifaceData.description }, nodeType, SymbolTypeFlag.typetime);
157,520✔
190
        }
191

192
        for (const componentData of Object.values(components) as BRSComponentData[]) {
1,790✔
193
            const nodeType = new InterfaceType(componentData.name);
116,350✔
194
            nodeType.addBuiltInInterfaces();
116,350✔
195
            nodeType.isBuiltIn = true;
116,350✔
196
            if (componentData.name !== 'roSGNode') {
116,350✔
197
                // we will add `roSGNode` as shorthand for `roSGNodeNode`, since all roSgNode components are SceneGraph nodes
198
                this.globalScope.symbolTable.addSymbol(componentData.name, { description: componentData.description }, nodeType, SymbolTypeFlag.typetime);
114,560✔
199
            }
200
        }
201

202
        for (const nodeData of Object.values(nodes) as SGNodeData[]) {
1,790✔
203
            this.recursivelyAddNodeToSymbolTable(nodeData);
171,840✔
204
        }
205

206
        for (const eventData of Object.values(events) as BRSEventData[]) {
1,790✔
207
            const nodeType = new InterfaceType(eventData.name);
32,220✔
208
            nodeType.addBuiltInInterfaces();
32,220✔
209
            nodeType.isBuiltIn = true;
32,220✔
210
            this.globalScope.symbolTable.addSymbol(eventData.name, { description: eventData.description }, nodeType, SymbolTypeFlag.typetime);
32,220✔
211
        }
212

213
    }
214

215
    /**
216
     * A graph of all files and their dependencies.
217
     * For example:
218
     *      File.xml -> [lib1.brs, lib2.brs]
219
     *      lib2.brs -> [lib3.brs] //via an import statement
220
     */
221
    private dependencyGraph = new DependencyGraph();
1,790✔
222

223
    public diagnostics: DiagnosticManager;
224

225
    /**
226
     * A scope that contains all built-in global functions.
227
     * All scopes should directly or indirectly inherit from this scope
228
     */
229
    public globalScope: Scope = undefined as any;
1,790✔
230

231
    /**
232
     * Plugins which can provide extra diagnostics or transform AST
233
     */
234
    public plugins: PluginInterface;
235

236
    private fileSymbolInformation = new Map<string, { provides: ProvidedSymbolInfo; requires: UnresolvedSymbol[] }>();
1,790✔
237

238

239
    private componentsTable = new SymbolTable('Custom Components');
1,790✔
240

241
    public addFileSymbolInfo(file: BrsFile) {
242
        this.fileSymbolInformation.set(file.pkgPath, {
1,667✔
243
            provides: file.providedSymbols,
244
            requires: file.requiredSymbols
245
        });
246
    }
247

248
    public getFileSymbolInfo(file: BrsFile) {
249
        return this.fileSymbolInformation.get(file.pkgPath);
1,670✔
250
    }
251

252
    /**
253
     * The path to bslib.brs (the BrightScript runtime for certain BrighterScript features)
254
     */
255
    public get bslibPkgPath() {
256
        //if there's an aliased (preferred) version of bslib from roku_modules loaded into the program, use that
257
        if (this.getFile(bslibAliasedRokuModulesPkgPath)) {
2,382✔
258
            return bslibAliasedRokuModulesPkgPath;
11✔
259

260
            //if there's a non-aliased version of bslib from roku_modules, use that
261
        } else if (this.getFile(bslibNonAliasedRokuModulesPkgPath)) {
2,371✔
262
            return bslibNonAliasedRokuModulesPkgPath;
24✔
263

264
            //default to the embedded version
265
        } else {
266
            return `${this.options.bslibDestinationDir}${path.sep}bslib.brs`;
2,347✔
267
        }
268
    }
269

270
    public get bslibPrefix() {
271
        if (this.bslibPkgPath === bslibNonAliasedRokuModulesPkgPath) {
1,737✔
272
            return 'rokucommunity_bslib';
18✔
273
        } else {
274
            return 'bslib';
1,719✔
275
        }
276
    }
277

278

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

294
    private scopes = {} as Record<string, Scope>;
1,790✔
295

296
    protected addScope(scope: Scope) {
297
        this.scopes[scope.name] = scope;
1,954✔
298
        delete this.sortedScopeNames;
1,954✔
299
    }
300

301
    protected removeScope(scope: Scope) {
302
        if (this.scopes[scope.name]) {
11!
303
            delete this.scopes[scope.name];
11✔
304
            delete this.sortedScopeNames;
11✔
305
        }
306
    }
307

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

316
    /**
317
     * Get the component with the specified name
318
     */
319
    public getComponent(componentName: string) {
320
        if (componentName) {
1,896✔
321
            //return the first compoment in the list with this name
322
            //(components are ordered in this list by destPath to ensure consistency)
323
            return this.components[componentName.toLowerCase()]?.[0];
1,882✔
324
        } else {
325
            return undefined;
14✔
326
        }
327
    }
328

329
    /**
330
     * Get the sorted names of custom components
331
     */
332
    public getSortedComponentNames() {
333
        const componentNames = Object.keys(this.components);
1,336✔
334
        componentNames.sort((a, b) => {
1,336✔
335
            if (a < b) {
703✔
336
                return -1;
279✔
337
            } else if (b < a) {
424!
338
                return 1;
424✔
339
            }
UNCOV
340
            return 0;
×
341
        });
342
        return componentNames;
1,336✔
343
    }
344

345
    /**
346
     * Keeps a set of all the components that need to have their types updated during the current validation cycle
347
     */
348
    private componentSymbolsToUpdate = new Set<{ componentKey: string; componentName: string }>();
1,790✔
349

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

376
    /**
377
     * Remove the specified component from the components map
378
     */
379
    private unregisterComponent(xmlFile: XmlFile) {
380
        const key = this.getComponentKey(xmlFile);
11✔
381
        const arr = this.components[key] || [];
11!
382
        for (let i = 0; i < arr.length; i++) {
11✔
383
            if (arr[i].file === xmlFile) {
11!
384
                arr.splice(i, 1);
11✔
385
                break;
11✔
386
            }
387
        }
388

389
        this.syncComponentDependencyGraph(arr);
11✔
390
        this.addDeferredComponentTypeSymbolCreation(xmlFile);
11✔
391
    }
392

393
    /**
394
     * Adds a component described in an XML to the set of components that needs to be updated this validation cycle.
395
     * @param xmlFile XML file with <component> tag
396
     */
397
    private addDeferredComponentTypeSymbolCreation(xmlFile: XmlFile) {
398
        this.componentSymbolsToUpdate.add({ componentKey: this.getComponentKey(xmlFile), componentName: xmlFile.componentName?.text });
411✔
399

400
    }
401

402
    private getComponentKey(xmlFile: XmlFile) {
403
        return (xmlFile.componentName?.text ?? xmlFile.pkgPath).toLowerCase();
822✔
404
    }
405

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

433
    /**
434
     * Adds a reference type to the global symbol table with the first component in this.components to have the same name as the component in the file
435
     * @param componentKey key getting a component from `this.components`
436
     * @param componentName the unprefixed name of the component that will be added (e.g. 'MyLabel' NOT 'roSgNodeMyLabel')
437
     */
438
    private addComponentReferenceType(componentKey: string, componentName: string) {
439
        const symbolName = componentName ? util.getSgNodeTypeName(componentName) : undefined;
330✔
440
        if (!symbolName) {
330✔
441
            return;
7✔
442
        }
443
        const components = this.components[componentKey] || [];
323!
444
        // Remove any existing symbols that match
445
        this.globalScope.symbolTable.removeSymbol(symbolName);
323✔
446
        // There is a component that can be added - use it.
447
        if (components.length > 0) {
323✔
448

449
            const componentRefType = new ReferenceType(symbolName, symbolName, SymbolTypeFlag.typetime, () => this.componentsTable);
2,091✔
450
            if (componentRefType) {
322!
451
                this.globalScope.symbolTable.addSymbol(symbolName, {}, componentRefType, SymbolTypeFlag.typetime);
322✔
452
            }
453

454
        }
455
    }
456

457
    /**
458
     * re-attach the dependency graph with a new key for any component who changed
459
     * their position in their own named array (only matters when there are multiple
460
     * components with the same name)
461
     */
462
    private syncComponentDependencyGraph(components: Array<{ file: XmlFile; scope: XmlScope }>) {
463
        //reattach every dependency graph
464
        for (let i = 0; i < components.length; i++) {
411✔
465
            const { file, scope } = components[i];
406✔
466

467
            //attach (or re-attach) the dependencyGraph for every component whose position changed
468
            if (file.dependencyGraphIndex !== i) {
406✔
469
                file.dependencyGraphIndex = i;
402✔
470
                this.dependencyGraph.addOrReplace(file.dependencyGraphKey, file.dependencies);
402✔
471
                file.attachDependencyGraph(this.dependencyGraph);
402✔
472
                scope.attachDependencyGraph(this.dependencyGraph);
402✔
473
            }
474
        }
475
    }
476

477
    /**
478
     * Get a list of all files that are included in the project but are not referenced
479
     * by any scope in the program.
480
     */
481
    public getUnreferencedFiles() {
UNCOV
482
        let result = [] as BscFile[];
×
UNCOV
483
        for (let filePath in this.files) {
×
UNCOV
484
            let file = this.files[filePath];
×
485
            //is this file part of a scope
UNCOV
486
            if (!this.getFirstScopeForFile(file)) {
×
487
                //no scopes reference this file. add it to the list
UNCOV
488
                result.push(file);
×
489
            }
490
        }
UNCOV
491
        return result;
×
492
    }
493

494
    /**
495
     * Get the list of errors for the entire program.
496
     */
497
    public getDiagnostics() {
498
        return this.diagnostics.getDiagnostics();
1,134✔
499
    }
500

501
    /**
502
     * Determine if the specified file is loaded in this program right now.
503
     * @param filePath the absolute or relative path to the file
504
     * @param normalizePath should the provided path be normalized before use
505
     */
506
    public hasFile(filePath: string, normalizePath = true) {
2,554✔
507
        return !!this.getFile(filePath, normalizePath);
2,554✔
508
    }
509

510
    /**
511
     * roku filesystem is case INsensitive, so find the scope by key case insensitive
512
     * @param scopeName xml scope names are their `destPath`. Source scope is stored with the key `"source"`
513
     */
514
    public getScopeByName(scopeName: string): Scope | undefined {
515
        if (!scopeName) {
60!
UNCOV
516
            return undefined;
×
517
        }
518
        //most scopes are xml file pkg paths. however, the ones that are not are single names like "global" and "scope",
519
        //so it's safe to run the standardizePkgPath method
520
        scopeName = s`${scopeName}`;
60✔
521
        let key = Object.keys(this.scopes).find(x => x.toLowerCase() === scopeName.toLowerCase());
137✔
522
        return this.scopes[key!];
60✔
523
    }
524

525
    /**
526
     * Return all scopes
527
     */
528
    public getScopes() {
529
        return Object.values(this.scopes);
10✔
530
    }
531

532
    /**
533
     * Find the scope for the specified component
534
     */
535
    public getComponentScope(componentName: string) {
536
        return this.getComponent(componentName)?.scope;
449✔
537
    }
538

539
    /**
540
     * Update internal maps with this file reference
541
     */
542
    private assignFile<T extends BscFile = BscFile>(file: T) {
543
        const fileAddEvent: BeforeFileAddEvent = {
2,375✔
544
            file: file,
545
            program: this
546
        };
547

548
        this.plugins.emit('beforeFileAdd', fileAddEvent);
2,375✔
549

550
        this.files[file.srcPath.toLowerCase()] = file;
2,375✔
551
        this.destMap.set(file.destPath.toLowerCase(), file);
2,375✔
552

553
        this.plugins.emit('afterFileAdd', fileAddEvent);
2,375✔
554

555
        return file;
2,375✔
556
    }
557

558
    /**
559
     * Remove this file from internal maps
560
     */
561
    private unassignFile<T extends BscFile = BscFile>(file: T) {
562
        delete this.files[file.srcPath.toLowerCase()];
152✔
563
        this.destMap.delete(file.destPath.toLowerCase());
152✔
564
        return file;
152✔
565
    }
566

567
    /**
568
     * Load a file into the program. If that file already exists, it is replaced.
569
     * If file contents are provided, those are used, Otherwise, the file is loaded from the file system
570
     * @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:/`)
571
     * @param fileData the file contents. omit or pass `undefined` to prevent loading the data at this time
572
     */
573
    public setFile<T extends BscFile>(srcDestOrPkgPath: string, fileData?: FileData): T;
574
    /**
575
     * Load a file into the program. If that file already exists, it is replaced.
576
     * @param fileEntry an object that specifies src and dest for the file.
577
     * @param fileData the file contents. omit or pass `undefined` to prevent loading the data at this time
578
     */
579
    public setFile<T extends BscFile>(fileEntry: FileObj, fileData: FileData): T;
580
    public setFile<T extends BscFile>(fileParam: FileObj | string, fileData: FileData): T {
581
        //normalize the file paths
582
        const { srcPath, destPath } = this.getPaths(fileParam, this.options.rootDir);
2,371✔
583

584
        let file = this.logger.time(LogLevel.debug, ['Program.setFile()', chalk.green(srcPath)], () => {
2,371✔
585
            //if the file is already loaded, remove it
586
            if (this.hasFile(srcPath)) {
2,371✔
587
                this.removeFile(srcPath, true, true);
136✔
588
            }
589

590
            const data = new LazyFileData(fileData);
2,371✔
591

592
            const event = new ProvideFileEventInternal(this, srcPath, destPath, data, this.fileFactory);
2,371✔
593

594
            this.plugins.emit('beforeProvideFile', event);
2,371✔
595
            this.plugins.emit('provideFile', event);
2,371✔
596
            this.plugins.emit('afterProvideFile', event);
2,371✔
597

598
            //if no files were provided, create a AssetFile to represent it.
599
            if (event.files.length === 0) {
2,371✔
600
                event.files.push(
18✔
601
                    this.fileFactory.AssetFile({
602
                        srcPath: event.srcPath,
603
                        destPath: event.destPath,
604
                        pkgPath: event.destPath,
605
                        data: data
606
                    })
607
                );
608
            }
609

610
            //find the file instance for the srcPath that triggered this action.
611
            const primaryFile = event.files.find(x => x.srcPath === srcPath);
2,371✔
612

613
            if (!primaryFile) {
2,371!
UNCOV
614
                throw new Error(`No file provided for srcPath '${srcPath}'. Instead, received ${JSON.stringify(event.files.map(x => ({
×
615
                    type: x.type,
616
                    srcPath: x.srcPath,
617
                    destPath: x.destPath
618
                })))}`);
619
            }
620

621
            //link the virtual files to the primary file
622
            this.fileClusters.set(primaryFile.srcPath?.toLowerCase(), event.files);
2,371!
623

624
            for (const file of event.files) {
2,371✔
625
                file.srcPath = s(file.srcPath);
2,375✔
626
                if (file.destPath) {
2,375!
627
                    file.destPath = s`${util.replaceCaseInsensitive(file.destPath, this.options.rootDir, '')}`;
2,375✔
628
                }
629
                if (file.pkgPath) {
2,375✔
630
                    file.pkgPath = s`${util.replaceCaseInsensitive(file.pkgPath, this.options.rootDir, '')}`;
2,371✔
631
                } else {
632
                    file.pkgPath = file.destPath;
4✔
633
                }
634
                file.excludeFromOutput = file.excludeFromOutput === true;
2,375✔
635

636
                //set the dependencyGraph key for every file to its destPath
637
                file.dependencyGraphKey = file.destPath.toLowerCase();
2,375✔
638

639
                this.assignFile(file);
2,375✔
640

641
                //register a callback anytime this file's dependencies change
642
                if (typeof file.onDependenciesChanged === 'function') {
2,375✔
643
                    file.disposables ??= [];
2,349!
644
                    file.disposables.push(
2,349✔
645
                        this.dependencyGraph.onchange(file.dependencyGraphKey, file.onDependenciesChanged.bind(file))
646
                    );
647
                }
648

649
                //register this file (and its dependencies) with the dependency graph
650
                this.dependencyGraph.addOrReplace(file.dependencyGraphKey, file.dependencies ?? []);
2,375✔
651

652
                //if this is a `source` file, add it to the source scope's dependency list
653
                if (this.isSourceBrsFile(file)) {
2,375✔
654
                    this.createSourceScope();
1,597✔
655
                    this.dependencyGraph.addDependency('scope:source', file.dependencyGraphKey);
1,597✔
656
                }
657

658
                //if this is an xml file in the components folder, register it as a component
659
                if (this.isComponentsXmlFile(file)) {
2,375✔
660
                    //create a new scope for this xml file
661
                    let scope = new XmlScope(file, this);
400✔
662
                    this.addScope(scope);
400✔
663

664
                    //register this compoent now that we have parsed it and know its component name
665
                    this.registerComponent(file, scope);
400✔
666

667
                    //notify plugins that the scope is created and the component is registered
668
                    this.plugins.emit('afterScopeCreate', {
400✔
669
                        program: this,
670
                        scope: scope
671
                    });
672
                }
673
            }
674

675
            return primaryFile;
2,371✔
676
        });
677
        return file as T;
2,371✔
678
    }
679

680
    /**
681
     * Given a srcPath, a destPath, or both, resolve whichever is missing, relative to rootDir.
682
     * @param fileParam an object representing file paths
683
     * @param rootDir must be a pre-normalized path
684
     */
685
    private getPaths(fileParam: string | FileObj | { srcPath?: string; pkgPath?: string }, rootDir: string) {
686
        let srcPath: string | undefined;
687
        let destPath: string | undefined;
688

689
        assert.ok(fileParam, 'fileParam is required');
2,526✔
690

691
        //lift the path vars from the incoming param
692
        if (typeof fileParam === 'string') {
2,526✔
693
            fileParam = this.removePkgPrefix(fileParam);
2,179✔
694
            srcPath = s`${path.resolve(rootDir, fileParam)}`;
2,179✔
695
            destPath = s`${util.replaceCaseInsensitive(srcPath, rootDir, '')}`;
2,179✔
696
        } else {
697
            let param: any = fileParam;
347✔
698

699
            if (param.src) {
347✔
700
                srcPath = s`${param.src}`;
346✔
701
            }
702
            if (param.srcPath) {
347!
UNCOV
703
                srcPath = s`${param.srcPath}`;
×
704
            }
705
            if (param.dest) {
347✔
706
                destPath = s`${this.removePkgPrefix(param.dest)}`;
346✔
707
            }
708
            if (param.pkgPath) {
347!
UNCOV
709
                destPath = s`${this.removePkgPrefix(param.pkgPath)}`;
×
710
            }
711
        }
712

713
        //if there's no srcPath, use the destPath to build an absolute srcPath
714
        if (!srcPath) {
2,526✔
715
            srcPath = s`${rootDir}/${destPath}`;
1✔
716
        }
717
        //coerce srcPath to an absolute path
718
        if (!path.isAbsolute(srcPath)) {
2,526✔
719
            srcPath = util.standardizePath(srcPath);
1✔
720
        }
721

722
        //if destPath isn't set, compute it from the other paths
723
        if (!destPath) {
2,526✔
724
            destPath = s`${util.replaceCaseInsensitive(srcPath, rootDir, '')}`;
1✔
725
        }
726

727
        assert.ok(srcPath, 'fileEntry.src is required');
2,526✔
728
        assert.ok(destPath, 'fileEntry.dest is required');
2,526✔
729

730
        return {
2,526✔
731
            srcPath: srcPath,
732
            //remove leading slash
733
            destPath: destPath.replace(/^[\/\\]+/, '')
734
        };
735
    }
736

737
    /**
738
     * Remove any leading `pkg:/` found in the path
739
     */
740
    private removePkgPrefix(path: string) {
741
        return path.replace(/^pkg:\//i, '');
2,525✔
742
    }
743

744
    /**
745
     * Is this file a .brs file found somewhere within the `pkg:/source/` folder?
746
     */
747
    private isSourceBrsFile(file: BscFile) {
748
        return !!/^(pkg:\/)?source[\/\\]/.exec(file.destPath);
2,527✔
749
    }
750

751
    /**
752
     * Is this file a .brs file found somewhere within the `pkg:/source/` folder?
753
     */
754
    private isComponentsXmlFile(file: BscFile): file is XmlFile {
755
        return isXmlFile(file) && !!/^(pkg:\/)?components[\/\\]/.exec(file.destPath);
2,375✔
756
    }
757

758
    /**
759
     * Ensure source scope is created.
760
     * Note: automatically called internally, and no-op if it exists already.
761
     */
762
    public createSourceScope() {
763
        if (!this.scopes.source) {
2,351✔
764
            const sourceScope = new Scope('source', this, 'scope:source');
1,554✔
765
            sourceScope.attachDependencyGraph(this.dependencyGraph);
1,554✔
766
            this.addScope(sourceScope);
1,554✔
767
            this.plugins.emit('afterScopeCreate', {
1,554✔
768
                program: this,
769
                scope: sourceScope
770
            });
771
        }
772
    }
773

774
    /**
775
     * Remove a set of files from the program
776
     * @param srcPaths can be an array of srcPath or destPath strings
777
     * @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
778
     */
779
    public removeFiles(srcPaths: string[], normalizePath = true) {
1✔
780
        for (let srcPath of srcPaths) {
1✔
781
            this.removeFile(srcPath, normalizePath);
1✔
782
        }
783
    }
784

785
    /**
786
     * Remove a file from the program
787
     * @param filePath can be a srcPath, a destPath, or a destPath with leading `pkg:/`
788
     * @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
789
     */
790
    public removeFile(filePath: string, normalizePath = true, keepSymbolInformation = false) {
27✔
791
        this.logger.debug('Program.removeFile()', filePath);
150✔
792
        const paths = this.getPaths(filePath, this.options.rootDir);
150✔
793

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

797
        for (const file of files) {
150✔
798
            //if a file has already been removed, nothing more needs to be done here
799
            if (!file || !this.hasFile(file.srcPath)) {
153✔
800
                continue;
1✔
801
            }
802
            this.diagnostics.clearForFile(file.srcPath);
152✔
803

804
            const event: BeforeFileRemoveEvent = { file: file, program: this };
152✔
805
            this.plugins.emit('beforeFileRemove', event);
152✔
806

807
            //if there is a scope named the same as this file's path, remove it (i.e. xml scopes)
808
            let scope = this.scopes[file.destPath];
152✔
809
            if (scope) {
152✔
810
                const scopeDisposeEvent = {
11✔
811
                    program: this,
812
                    scope: scope
813
                };
814
                this.plugins.emit('beforeScopeDispose', scopeDisposeEvent);
11✔
815
                this.plugins.emit('onScopeDispose', scopeDisposeEvent);
11✔
816
                scope.dispose();
11✔
817
                //notify dependencies of this scope that it has been removed
818
                this.dependencyGraph.remove(scope.dependencyGraphKey!);
11✔
819
                this.removeScope(this.scopes[file.destPath]);
11✔
820
                this.plugins.emit('afterScopeDispose', scopeDisposeEvent);
11✔
821
            }
822
            //remove the file from the program
823
            this.unassignFile(file);
152✔
824

825
            this.dependencyGraph.remove(file.dependencyGraphKey);
152✔
826

827
            //if this is a pkg:/source file, notify the `source` scope that it has changed
828
            if (this.isSourceBrsFile(file)) {
152✔
829
                this.dependencyGraph.removeDependency('scope:source', file.dependencyGraphKey);
126✔
830
            }
831
            if (isBrsFile(file)) {
152✔
832
                if (!keepSymbolInformation) {
135✔
833
                    this.fileSymbolInformation.delete(file.pkgPath);
8✔
834
                }
835
                this.crossScopeValidation.clearResolutionsForFile(file);
135✔
836
            }
837

838
            //if this is a component, remove it from our components map
839
            if (isXmlFile(file)) {
152✔
840
                this.unregisterComponent(file);
11✔
841
            }
842
            //dispose any disposable things on the file
843
            for (const disposable of file?.disposables ?? []) {
152!
844
                disposable();
146✔
845
            }
846
            //dispose file
847
            file?.dispose?.();
152!
848

849
            this.plugins.emit('afterFileRemove', event);
152✔
850
        }
851
    }
852

853
    public crossScopeValidation = new CrossScopeValidator(this);
1,790✔
854

855
    private isFirstValidation = true;
1,790✔
856

857
    /**
858
     * Traverse the entire project, and validate all scopes
859
     */
860
    public validate() {
861
        this.logger.time(LogLevel.log, ['Validating project'], () => {
1,336✔
862
            this.diagnostics.clearForTag(ProgramValidatorDiagnosticsTag);
1,336✔
863
            const programValidateEvent = {
1,336✔
864
                program: this
865
            };
866
            this.plugins.emit('beforeProgramValidate', programValidateEvent);
1,336✔
867
            this.plugins.emit('onProgramValidate', programValidateEvent);
1,336✔
868

869
            const metrics = {
1,336✔
870
                filesChanged: 0,
871
                filesValidated: 0,
872
                fileValidationTime: '',
873
                crossScopeValidationTime: '',
874
                scopesValidated: 0,
875
                totalLinkTime: '',
876
                totalScopeValidationTime: '',
877
                componentValidationTime: ''
878
            };
879

880
            const validationStopwatch = new Stopwatch();
1,336✔
881
            //validate every file
882
            const brsFilesValidated: BrsFile[] = [];
1,336✔
883
            const afterValidateFiles: BscFile[] = [];
1,336✔
884

885
            // Create reference component types for any component that changes
886
            this.logger.time(LogLevel.info, ['Build component types'], () => {
1,336✔
887
                for (let { componentKey, componentName } of this.componentSymbolsToUpdate) {
1,336✔
888
                    this.addComponentReferenceType(componentKey, componentName);
330✔
889
                }
890
            });
891

892

893
            metrics.fileValidationTime = validationStopwatch.getDurationTextFor(() => {
1,336✔
894
                //sort files by path so we get consistent results
895
                const files = Object.values(this.files).sort(firstBy(x => x.srcPath));
3,624✔
896
                for (const file of files) {
1,336✔
897
                    //for every unvalidated file, validate it
898
                    if (!file.isValidated) {
2,268✔
899
                        const validateFileEvent = {
1,936✔
900
                            program: this,
901
                            file: file
902
                        };
903
                        this.plugins.emit('beforeFileValidate', validateFileEvent);
1,936✔
904
                        //emit an event to allow plugins to contribute to the file validation process
905
                        this.plugins.emit('onFileValidate', validateFileEvent);
1,936✔
906
                        file.isValidated = true;
1,936✔
907
                        if (isBrsFile(file)) {
1,936✔
908
                            brsFilesValidated.push(file);
1,609✔
909
                        }
910
                        afterValidateFiles.push(file);
1,936✔
911
                    }
912
                }
913
                // AfterFileValidate is after all files have been validated
914
                for (const file of afterValidateFiles) {
1,336✔
915
                    const validateFileEvent = {
1,936✔
916
                        program: this,
917
                        file: file
918
                    };
919
                    this.plugins.emit('afterFileValidate', validateFileEvent);
1,936✔
920
                }
921
            }).durationText;
922

923
            metrics.filesChanged = afterValidateFiles.length;
1,336✔
924

925
            // Build component types for any component that changes
926
            this.logger.time(LogLevel.info, ['Build component types'], () => {
1,336✔
927
                for (let { componentKey, componentName } of this.componentSymbolsToUpdate) {
1,336✔
928
                    this.updateComponentSymbolInGlobalScope(componentKey, componentName);
330✔
929
                }
930
                this.componentSymbolsToUpdate.clear();
1,336✔
931
            });
932

933

934
            const changedSymbolsMapArr = brsFilesValidated?.map(f => {
1,336!
935
                if (isBrsFile(f)) {
1,609!
936
                    return f.providedSymbols.changes;
1,609✔
937
                }
UNCOV
938
                return null;
×
939
            }).filter(x => x);
1,609✔
940

941
            const changedSymbols = new Map<SymbolTypeFlag, Set<string>>();
1,336✔
942
            for (const flag of [SymbolTypeFlag.runtime, SymbolTypeFlag.typetime]) {
1,336✔
943
                const changedSymbolsSetArr = changedSymbolsMapArr.map(symMap => symMap.get(flag));
3,218✔
944
                changedSymbols.set(flag, new Set(...changedSymbolsSetArr));
2,672✔
945
            }
946

947
            const filesToBeValidatedInScopeContext = new Set<BscFile>(afterValidateFiles);
1,336✔
948

949
            metrics.crossScopeValidationTime = validationStopwatch.getDurationTextFor(() => {
1,336✔
950
                const scopesToCheck = this.getScopesForCrossScopeValidation();
1,336✔
951
                this.crossScopeValidation.buildComponentsMap();
1,336✔
952
                this.crossScopeValidation.addDiagnosticsForScopes(scopesToCheck);
1,336✔
953
                const filesToRevalidate = this.crossScopeValidation.getFilesRequiringChangedSymbol(scopesToCheck, changedSymbols);
1,336✔
954
                for (const file of filesToRevalidate) {
1,336✔
955
                    filesToBeValidatedInScopeContext.add(file);
178✔
956
                }
957
            }).durationText;
958

959
            metrics.filesValidated = filesToBeValidatedInScopeContext.size;
1,336✔
960

961
            let linkTime = 0;
1,336✔
962
            let validationTime = 0;
1,336✔
963
            let scopesValidated = 0;
1,336✔
964
            let changedFiles = new Set<BscFile>(afterValidateFiles);
1,336✔
965
            this.logger.time(LogLevel.info, ['Validate all scopes'], () => {
1,336✔
966
                //sort the scope names so we get consistent results
967
                const scopeNames = this.getSortedScopeNames();
1,336✔
968
                for (const file of filesToBeValidatedInScopeContext) {
1,336✔
969
                    if (isBrsFile(file)) {
2,057✔
970
                        file.validationSegmenter.unValidateAllSegments();
1,730✔
971
                    }
972
                }
973
                for (let scopeName of scopeNames) {
1,336✔
974
                    let scope = this.scopes[scopeName];
3,042✔
975
                    const scopeValidated = scope.validate({
3,042✔
976
                        filesToBeValidatedInScopeContext: filesToBeValidatedInScopeContext,
977
                        changedSymbols: changedSymbols,
978
                        changedFiles: changedFiles,
979
                        initialValidation: this.isFirstValidation
980
                    });
981
                    if (scopeValidated) {
3,042✔
982
                        scopesValidated++;
1,656✔
983
                    }
984
                    linkTime += scope.validationMetrics.linkTime;
3,042✔
985
                    validationTime += scope.validationMetrics.validationTime;
3,042✔
986
                }
987
            });
988
            metrics.scopesValidated = scopesValidated;
1,336✔
989
            validationStopwatch.totalMilliseconds = linkTime;
1,336✔
990
            metrics.totalLinkTime = validationStopwatch.getDurationText();
1,336✔
991

992
            validationStopwatch.totalMilliseconds = validationTime;
1,336✔
993
            metrics.totalScopeValidationTime = validationStopwatch.getDurationText();
1,336✔
994

995
            metrics.componentValidationTime = validationStopwatch.getDurationTextFor(() => {
1,336✔
996
                this.detectDuplicateComponentNames();
1,336✔
997
            }).durationText;
998

999
            this.logValidationMetrics(metrics);
1,336✔
1000

1001
            this.isFirstValidation = false;
1,336✔
1002

1003
            this.plugins.emit('afterProgramValidate', programValidateEvent);
1,336✔
1004
        });
1005
    }
1006

1007
    // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
1008
    private logValidationMetrics(metrics: { [key: string]: number | string }) {
1009
        let logs = [] as string[];
1,336✔
1010
        for (const key in metrics) {
1,336✔
1011
            logs.push(`${key}=${chalk.yellow(metrics[key].toString())}`);
10,688✔
1012
        }
1013
        this.logger.info(`Validation Metrics: ${logs.join(', ')}`);
1,336✔
1014
    }
1015

1016
    private getScopesForCrossScopeValidation() {
1017
        const scopesForCrossScopeValidation = [];
1,336✔
1018
        for (let scopeName of this.getSortedScopeNames()) {
1,336✔
1019
            let scope = this.scopes[scopeName];
3,042✔
1020
            if (this.globalScope !== scope && !scope.isValidated) {
3,042✔
1021
                scopesForCrossScopeValidation.push(scope);
1,677✔
1022
            }
1023
        }
1024
        return scopesForCrossScopeValidation;
1,336✔
1025
    }
1026

1027
    /**
1028
     * Flag all duplicate component names
1029
     */
1030
    private detectDuplicateComponentNames() {
1031
        const componentsByName = Object.keys(this.files).reduce<Record<string, XmlFile[]>>((map, filePath) => {
1,336✔
1032
            const file = this.files[filePath];
2,268✔
1033
            //if this is an XmlFile, and it has a valid `componentName` property
1034
            if (isXmlFile(file) && file.componentName?.text) {
2,268✔
1035
                let lowerName = file.componentName.text.toLowerCase();
459✔
1036
                if (!map[lowerName]) {
459✔
1037
                    map[lowerName] = [];
456✔
1038
                }
1039
                map[lowerName].push(file);
459✔
1040
            }
1041
            return map;
2,268✔
1042
        }, {});
1043

1044
        for (let name in componentsByName) {
1,336✔
1045
            const xmlFiles = componentsByName[name];
456✔
1046
            //add diagnostics for every duplicate component with this name
1047
            if (xmlFiles.length > 1) {
456✔
1048
                for (let xmlFile of xmlFiles) {
3✔
1049
                    const { componentName } = xmlFile;
6✔
1050
                    this.diagnostics.register({
6✔
1051
                        ...DiagnosticMessages.duplicateComponentName(componentName.text),
1052
                        location: xmlFile.componentName.location,
1053
                        relatedInformation: xmlFiles.filter(x => x !== xmlFile).map(x => {
12✔
1054
                            return {
6✔
1055
                                location: x.componentName.location,
1056
                                message: 'Also defined here'
1057
                            };
1058
                        })
1059
                    }, { tags: [ProgramValidatorDiagnosticsTag] });
1060
                }
1061
            }
1062
        }
1063
    }
1064

1065
    /**
1066
     * Get the files for a list of filePaths
1067
     * @param filePaths can be an array of srcPath or a destPath strings
1068
     * @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
1069
     */
1070
    public getFiles<T extends BscFile>(filePaths: string[], normalizePath = true) {
29✔
1071
        return filePaths
29✔
1072
            .map(filePath => this.getFile(filePath, normalizePath))
39✔
1073
            .filter(file => file !== undefined) as T[];
39✔
1074
    }
1075

1076
    /**
1077
     * Get the file at the given path
1078
     * @param filePath can be a srcPath or a destPath
1079
     * @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
1080
     */
1081
    public getFile<T extends BscFile>(filePath: string, normalizePath = true) {
14,236✔
1082
        if (typeof filePath !== 'string') {
18,257✔
1083
            return undefined;
3,363✔
1084
            //is the path absolute (or the `virtual:` prefix)
1085
        } else if (/^(?:(?:virtual:[\/\\])|(?:\w:)|(?:[\/\\]))/gmi.exec(filePath)) {
14,894✔
1086
            return this.files[
4,579✔
1087
                (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase()
4,579!
1088
            ] as T;
1089
        } else if (util.isUriLike(filePath)) {
10,315✔
1090
            const path = URI.parse(filePath).fsPath;
687✔
1091
            return this.files[
687✔
1092
                (normalizePath ? util.standardizePath(path) : path).toLowerCase()
687!
1093
            ] as T;
1094
        } else {
1095
            return this.destMap.get(
9,628✔
1096
                (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase()
9,628✔
1097
            ) as T;
1098
        }
1099
    }
1100

1101
    private sortedScopeNames: string[] = undefined;
1,790✔
1102

1103
    /**
1104
     * Gets a sorted list of all scopeNames, always beginning with "global", "source", then any others in alphabetical order
1105
     */
1106
    private getSortedScopeNames() {
1107
        if (!this.sortedScopeNames) {
7,356✔
1108
            this.sortedScopeNames = Object.keys(this.scopes).sort((a, b) => {
1,291✔
1109
                if (a === 'global') {
1,855!
UNCOV
1110
                    return -1;
×
1111
                } else if (b === 'global') {
1,855✔
1112
                    return 1;
1,273✔
1113
                }
1114
                if (a === 'source') {
582✔
1115
                    return -1;
27✔
1116
                } else if (b === 'source') {
555✔
1117
                    return 1;
126✔
1118
                }
1119
                if (a < b) {
429✔
1120
                    return -1;
179✔
1121
                } else if (b < a) {
250!
1122
                    return 1;
250✔
1123
                }
UNCOV
1124
                return 0;
×
1125
            });
1126
        }
1127
        return this.sortedScopeNames;
7,356✔
1128
    }
1129

1130
    /**
1131
     * Get a list of all scopes the file is loaded into
1132
     * @param file the file
1133
     */
1134
    public getScopesForFile(file: BscFile | string) {
1135
        const resolvedFile = typeof file === 'string' ? this.getFile(file) : file;
720✔
1136

1137
        let result = [] as Scope[];
720✔
1138
        if (resolvedFile) {
720✔
1139
            const scopeKeys = this.getSortedScopeNames();
719✔
1140
            for (let key of scopeKeys) {
719✔
1141
                let scope = this.scopes[key];
1,507✔
1142

1143
                if (scope.hasFile(resolvedFile)) {
1,507✔
1144
                    result.push(scope);
733✔
1145
                }
1146
            }
1147
        }
1148
        return result;
720✔
1149
    }
1150

1151
    /**
1152
     * Get the first found scope for a file.
1153
     */
1154
    public getFirstScopeForFile(file: BscFile): Scope | undefined {
1155
        const scopeKeys = this.getSortedScopeNames();
3,965✔
1156
        for (let key of scopeKeys) {
3,965✔
1157
            let scope = this.scopes[key];
18,307✔
1158

1159
            if (scope.hasFile(file)) {
18,307✔
1160
                return scope;
2,890✔
1161
            }
1162
        }
1163
    }
1164

1165
    public getStatementsByName(name: string, originFile: BrsFile, namespaceName?: string): FileLink<Statement>[] {
1166
        let results = new Map<Statement, FileLink<Statement>>();
39✔
1167
        const filesSearched = new Set<BrsFile>();
39✔
1168
        let lowerNamespaceName = namespaceName?.toLowerCase();
39✔
1169
        let lowerName = name?.toLowerCase();
39!
1170

1171
        function addToResults(statement: FunctionStatement | MethodStatement, file: BrsFile) {
1172
            let parentNamespaceName = statement.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(originFile.parseMode)?.toLowerCase();
98✔
1173
            if (statement.tokens.name.text.toLowerCase() === lowerName && (!lowerNamespaceName || parentNamespaceName === lowerNamespaceName)) {
98✔
1174
                if (!results.has(statement)) {
36!
1175
                    results.set(statement, { item: statement, file: file as BrsFile });
36✔
1176
                }
1177
            }
1178
        }
1179

1180
        //look through all files in scope for matches
1181
        for (const scope of this.getScopesForFile(originFile)) {
39✔
1182
            for (const file of scope.getAllFiles()) {
39✔
1183
                //skip non-brs files, or files we've already processed
1184
                if (!isBrsFile(file) || filesSearched.has(file)) {
45✔
1185
                    continue;
3✔
1186
                }
1187
                filesSearched.add(file);
42✔
1188

1189
                file.ast.walk(createVisitor({
42✔
1190
                    FunctionStatement: (statement: FunctionStatement) => {
1191
                        addToResults(statement, file);
95✔
1192
                    },
1193
                    MethodStatement: (statement: MethodStatement) => {
1194
                        addToResults(statement, file);
3✔
1195
                    }
1196
                }), {
1197
                    walkMode: WalkMode.visitStatements
1198
                });
1199
            }
1200
        }
1201
        return [...results.values()];
39✔
1202
    }
1203

1204
    public getStatementsForXmlFile(scope: XmlScope, filterName?: string): FileLink<FunctionStatement>[] {
1205
        let results = new Map<Statement, FileLink<FunctionStatement>>();
8✔
1206
        const filesSearched = new Set<BrsFile>();
8✔
1207

1208
        //get all function names for the xml file and parents
1209
        let funcNames = new Set<string>();
8✔
1210
        let currentScope = scope;
8✔
1211
        while (isXmlScope(currentScope)) {
8✔
1212
            for (let name of currentScope.xmlFile.ast.componentElement.interfaceElement?.functions.map((f) => f.name) ?? []) {
14✔
1213
                if (!filterName || name === filterName) {
14!
1214
                    funcNames.add(name);
14✔
1215
                }
1216
            }
1217
            currentScope = currentScope.getParentScope() as XmlScope;
10✔
1218
        }
1219

1220
        //look through all files in scope for matches
1221
        for (const file of scope.getOwnFiles()) {
8✔
1222
            //skip non-brs files, or files we've already processed
1223
            if (!isBrsFile(file) || filesSearched.has(file)) {
16✔
1224
                continue;
8✔
1225
            }
1226
            filesSearched.add(file);
8✔
1227

1228
            file.ast.walk(createVisitor({
8✔
1229
                FunctionStatement: (statement: FunctionStatement) => {
1230
                    if (funcNames.has(statement.tokens.name.text)) {
13!
1231
                        if (!results.has(statement)) {
13!
1232
                            results.set(statement, { item: statement, file: file });
13✔
1233
                        }
1234
                    }
1235
                }
1236
            }), {
1237
                walkMode: WalkMode.visitStatements
1238
            });
1239
        }
1240
        return [...results.values()];
8✔
1241
    }
1242

1243
    /**
1244
     * Find all available completion items at the given position
1245
     * @param filePath can be a srcPath or a destPath
1246
     * @param position the position (line & column) where completions should be found
1247
     */
1248
    public getCompletions(filePath: string, position: Position) {
1249
        let file = this.getFile(filePath);
117✔
1250
        if (!file) {
117!
UNCOV
1251
            return [];
×
1252
        }
1253

1254
        //find the scopes for this file
1255
        let scopes = this.getScopesForFile(file);
117✔
1256

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

1260
        const event: ProvideCompletionsEvent = {
117✔
1261
            program: this,
1262
            file: file,
1263
            scopes: scopes,
1264
            position: position,
1265
            completions: []
1266
        };
1267

1268
        this.plugins.emit('beforeProvideCompletions', event);
117✔
1269

1270
        this.plugins.emit('provideCompletions', event);
117✔
1271

1272
        this.plugins.emit('afterProvideCompletions', event);
117✔
1273

1274
        return event.completions;
117✔
1275
    }
1276

1277
    /**
1278
     * Goes through each file and builds a list of workspace symbols for the program. Used by LanguageServer's onWorkspaceSymbol functionality
1279
     */
1280
    public getWorkspaceSymbols() {
1281
        const event: ProvideWorkspaceSymbolsEvent = {
22✔
1282
            program: this,
1283
            workspaceSymbols: []
1284
        };
1285
        this.plugins.emit('beforeProvideWorkspaceSymbols', event);
22✔
1286
        this.plugins.emit('provideWorkspaceSymbols', event);
22✔
1287
        this.plugins.emit('afterProvideWorkspaceSymbols', event);
22✔
1288
        return event.workspaceSymbols;
22✔
1289
    }
1290

1291
    /**
1292
     * Given a position in a file, if the position is sitting on some type of identifier,
1293
     * go to the definition of that identifier (where this thing was first defined)
1294
     */
1295
    public getDefinition(srcPath: string, position: Position): Location[] {
1296
        let file = this.getFile(srcPath);
18✔
1297
        if (!file) {
18!
UNCOV
1298
            return [];
×
1299
        }
1300

1301
        const event: ProvideDefinitionEvent = {
18✔
1302
            program: this,
1303
            file: file,
1304
            position: position,
1305
            definitions: []
1306
        };
1307

1308
        this.plugins.emit('beforeProvideDefinition', event);
18✔
1309
        this.plugins.emit('provideDefinition', event);
18✔
1310
        this.plugins.emit('afterProvideDefinition', event);
18✔
1311
        return event.definitions;
18✔
1312
    }
1313

1314
    /**
1315
     * Get hover information for a file and position
1316
     */
1317
    public getHover(srcPath: string, position: Position): Hover[] {
1318
        let file = this.getFile(srcPath);
68✔
1319
        let result: Hover[];
1320
        if (file) {
68!
1321
            const event = {
68✔
1322
                program: this,
1323
                file: file,
1324
                position: position,
1325
                scopes: this.getScopesForFile(file),
1326
                hovers: []
1327
            } as ProvideHoverEvent;
1328
            this.plugins.emit('beforeProvideHover', event);
68✔
1329
            this.plugins.emit('provideHover', event);
68✔
1330
            this.plugins.emit('afterProvideHover', event);
68✔
1331
            result = event.hovers;
68✔
1332
        }
1333

1334
        return result ?? [];
68!
1335
    }
1336

1337
    /**
1338
     * Get full list of document symbols for a file
1339
     * @param srcPath path to the file
1340
     */
1341
    public getDocumentSymbols(srcPath: string): DocumentSymbol[] | undefined {
1342
        let file = this.getFile(srcPath);
18✔
1343
        if (file) {
18!
1344
            const event: ProvideDocumentSymbolsEvent = {
18✔
1345
                program: this,
1346
                file: file,
1347
                documentSymbols: []
1348
            };
1349
            this.plugins.emit('beforeProvideDocumentSymbols', event);
18✔
1350
            this.plugins.emit('provideDocumentSymbols', event);
18✔
1351
            this.plugins.emit('afterProvideDocumentSymbols', event);
18✔
1352
            return event.documentSymbols;
18✔
1353
        } else {
UNCOV
1354
            return undefined;
×
1355
        }
1356
    }
1357

1358
    /**
1359
     * Compute code actions for the given file and range
1360
     */
1361
    public getCodeActions(srcPath: string, range: Range) {
1362
        const codeActions = [] as CodeAction[];
13✔
1363
        const file = this.getFile(srcPath);
13✔
1364
        if (file) {
13✔
1365
            const fileUri = util.pathToUri(file?.srcPath);
12!
1366
            const diagnostics = this
12✔
1367
                //get all current diagnostics (filtered by diagnostic filters)
1368
                .getDiagnostics()
1369
                //only keep diagnostics related to this file
1370
                .filter(x => x.location?.uri === fileUri)
25✔
1371
                //only keep diagnostics that touch this range
1372
                .filter(x => util.rangesIntersectOrTouch(x.location.range, range));
12✔
1373

1374
            const scopes = this.getScopesForFile(file);
12✔
1375

1376
            this.plugins.emit('onGetCodeActions', {
12✔
1377
                program: this,
1378
                file: file,
1379
                range: range,
1380
                diagnostics: diagnostics,
1381
                scopes: scopes,
1382
                codeActions: codeActions
1383
            });
1384
        }
1385
        return codeActions;
13✔
1386
    }
1387

1388
    /**
1389
     * Get semantic tokens for the specified file
1390
     */
1391
    public getSemanticTokens(srcPath: string): SemanticToken[] | undefined {
1392
        const file = this.getFile(srcPath);
24✔
1393
        if (file) {
24!
1394
            const result = [] as SemanticToken[];
24✔
1395
            this.plugins.emit('onGetSemanticTokens', {
24✔
1396
                program: this,
1397
                file: file,
1398
                scopes: this.getScopesForFile(file),
1399
                semanticTokens: result
1400
            });
1401
            return result;
24✔
1402
        }
1403
    }
1404

1405
    public getSignatureHelp(filepath: string, position: Position): SignatureInfoObj[] {
1406
        let file: BrsFile = this.getFile(filepath);
185✔
1407
        if (!file || !isBrsFile(file)) {
185✔
1408
            return [];
3✔
1409
        }
1410
        let callExpressionInfo = new CallExpressionInfo(file, position);
182✔
1411
        let signatureHelpUtil = new SignatureHelpUtil();
182✔
1412
        return signatureHelpUtil.getSignatureHelpItems(callExpressionInfo);
182✔
1413
    }
1414

1415
    public getReferences(srcPath: string, position: Position): Location[] {
1416
        //find the file
1417
        let file = this.getFile(srcPath);
4✔
1418

1419
        const event: ProvideReferencesEvent = {
4✔
1420
            program: this,
1421
            file: file,
1422
            position: position,
1423
            references: []
1424
        };
1425

1426
        this.plugins.emit('beforeProvideReferences', event);
4✔
1427
        this.plugins.emit('provideReferences', event);
4✔
1428
        this.plugins.emit('afterProvideReferences', event);
4✔
1429

1430
        return event.references;
4✔
1431
    }
1432

1433
    /**
1434
     * Transpile a single file and get the result as a string.
1435
     * This does not write anything to the file system.
1436
     *
1437
     * This should only be called by `LanguageServer`.
1438
     * Internal usage should call `_getTranspiledFileContents` instead.
1439
     * @param filePath can be a srcPath or a destPath
1440
     */
1441
    public async getTranspiledFileContents(filePath: string): Promise<FileTranspileResult> {
1442
        const file = this.getFile(filePath);
306✔
1443

1444
        return this.getTranspiledFileContentsPipeline.run(async () => {
306✔
1445

1446
            const result = {
306✔
1447
                destPath: file.destPath,
1448
                pkgPath: file.pkgPath,
1449
                srcPath: file.srcPath
1450
            } as FileTranspileResult;
1451

1452
            const expectedPkgPath = file.pkgPath.toLowerCase();
306✔
1453
            const expectedMapPath = `${expectedPkgPath}.map`;
306✔
1454
            const expectedTypedefPkgPath = expectedPkgPath.replace(/\.brs$/i, '.d.bs');
306✔
1455

1456
            //add a temporary plugin to tap into the file writing process
1457
            const plugin = this.plugins.addFirst({
306✔
1458
                name: 'getTranspiledFileContents',
1459
                beforeWriteFile: (event) => {
1460
                    const pkgPath = event.file.pkgPath.toLowerCase();
968✔
1461
                    switch (pkgPath) {
968✔
1462
                        //this is the actual transpiled file
1463
                        case expectedPkgPath:
968✔
1464
                            result.code = event.file.data.toString();
306✔
1465
                            break;
306✔
1466
                        //this is the sourcemap
1467
                        case expectedMapPath:
1468
                            result.map = event.file.data.toString();
170✔
1469
                            break;
170✔
1470
                        //this is the typedef
1471
                        case expectedTypedefPkgPath:
1472
                            result.typedef = event.file.data.toString();
8✔
1473
                            break;
8✔
1474
                        default:
1475
                        //no idea what this file is. just ignore it
1476
                    }
1477
                    //mark every file as processed so it they don't get written to the output directory
1478
                    event.processedFiles.add(event.file);
968✔
1479
                }
1480
            });
1481

1482
            try {
306✔
1483
                //now that the plugin has been registered, run the build with just this file
1484
                await this.build({
306✔
1485
                    files: [file]
1486
                });
1487
            } finally {
1488
                this.plugins.remove(plugin);
306✔
1489
            }
1490
            return result;
306✔
1491
        });
1492
    }
1493
    private getTranspiledFileContentsPipeline = new ActionPipeline();
1,790✔
1494

1495
    /**
1496
     * Get the absolute output path for a file
1497
     */
1498
    private getOutputPath(file: { pkgPath?: string }, stagingDir = this.getStagingDir()) {
×
1499
        return s`${stagingDir}/${file.pkgPath}`;
1,783✔
1500
    }
1501

1502
    private getStagingDir(stagingDir?: string) {
1503
        let result = stagingDir ?? this.options.stagingDir ?? this.options.stagingDir;
693✔
1504
        if (!result) {
693✔
1505
            result = rokuDeploy.getOptions(this.options as any).stagingDir;
507✔
1506
        }
1507
        result = s`${path.resolve(this.options.cwd ?? process.cwd(), result ?? '/')}`;
693!
1508
        return result;
693✔
1509
    }
1510

1511
    /**
1512
     * Prepare the program for building
1513
     * @param files the list of files that should be prepared
1514
     */
1515
    private async prepare(files: BscFile[]) {
1516
        const programEvent: PrepareProgramEvent = {
347✔
1517
            program: this,
1518
            editor: this.editor,
1519
            files: files
1520
        };
1521

1522
        //assign an editor to every file
1523
        for (const file of programEvent.files) {
347✔
1524
            //if the file doesn't have an editor yet, assign one now
1525
            if (!file.editor) {
704✔
1526
                file.editor = new Editor();
657✔
1527
            }
1528
        }
1529

1530
        //sort the entries to make transpiling more deterministic
1531
        programEvent.files.sort((a, b) => {
347✔
1532
            if (a.pkgPath < b.pkgPath) {
371✔
1533
                return -1;
311✔
1534
            } else if (a.pkgPath > b.pkgPath) {
60!
1535
                return 1;
60✔
1536
            } else {
UNCOV
1537
                return 1;
×
1538
            }
1539
        });
1540

1541
        await this.plugins.emitAsync('beforePrepareProgram', programEvent);
347✔
1542
        await this.plugins.emitAsync('prepareProgram', programEvent);
347✔
1543

1544
        const stagingDir = this.getStagingDir();
347✔
1545

1546
        const entries: TranspileObj[] = [];
347✔
1547

1548
        for (const file of files) {
347✔
1549
            const scope = this.getFirstScopeForFile(file);
704✔
1550
            //link the symbol table for all the files in this scope
1551
            scope?.linkSymbolTable();
704✔
1552

1553
            //if the file doesn't have an editor yet, assign one now
1554
            if (!file.editor) {
704!
UNCOV
1555
                file.editor = new Editor();
×
1556
            }
1557
            const event = {
704✔
1558
                program: this,
1559
                file: file,
1560
                editor: file.editor,
1561
                scope: scope,
1562
                outputPath: this.getOutputPath(file, stagingDir)
1563
            } as PrepareFileEvent & { outputPath: string };
1564

1565
            await this.plugins.emitAsync('beforePrepareFile', event);
704✔
1566
            await this.plugins.emitAsync('prepareFile', event);
704✔
1567
            await this.plugins.emitAsync('afterPrepareFile', event);
704✔
1568

1569
            //TODO remove this in v1
1570
            entries.push(event);
704✔
1571

1572
            //unlink the symbolTable so the next loop iteration can link theirs
1573
            scope?.unlinkSymbolTable();
704✔
1574
        }
1575

1576
        await this.plugins.emitAsync('afterPrepareProgram', programEvent);
347✔
1577
        return files;
347✔
1578
    }
1579

1580
    /**
1581
     * Generate the contents of every file
1582
     */
1583
    private async serialize(files: BscFile[]) {
1584

1585
        const allFiles = new Map<BscFile, SerializedFile[]>();
346✔
1586

1587
        //exclude prunable files if that option is enabled
1588
        if (this.options.pruneEmptyCodeFiles === true) {
346✔
1589
            files = files.filter(x => x.canBePruned !== true);
9✔
1590
        }
1591

1592
        const serializeProgramEvent = await this.plugins.emitAsync('beforeSerializeProgram', {
346✔
1593
            program: this,
1594
            files: files,
1595
            result: allFiles
1596
        });
1597
        await this.plugins.emitAsync('onSerializeProgram', serializeProgramEvent);
346✔
1598

1599
        // serialize each file
1600
        for (const file of files) {
346✔
1601
            let scope = this.getFirstScopeForFile(file);
701✔
1602

1603
            //if the file doesn't have a scope, create a temporary scope for the file so it can depend on scope-level items
1604
            if (!scope) {
701✔
1605
                scope = new Scope(`temporary-for-${file.pkgPath}`, this);
357✔
1606
                scope.getAllFiles = () => [file];
3,200✔
1607
                scope.getOwnFiles = scope.getAllFiles;
357✔
1608
            }
1609

1610
            //link the symbol table for all the files in this scope
1611
            scope?.linkSymbolTable();
701!
1612
            const event: SerializeFileEvent = {
701✔
1613
                program: this,
1614
                file: file,
1615
                scope: scope,
1616
                result: allFiles
1617
            };
1618
            await this.plugins.emitAsync('beforeSerializeFile', event);
701✔
1619
            await this.plugins.emitAsync('serializeFile', event);
701✔
1620
            await this.plugins.emitAsync('afterSerializeFile', event);
701✔
1621
            //unlink the symbolTable so the next loop iteration can link theirs
1622
            scope?.unlinkSymbolTable();
701!
1623
        }
1624

1625
        this.plugins.emit('afterSerializeProgram', serializeProgramEvent);
346✔
1626

1627
        return allFiles;
346✔
1628
    }
1629

1630
    /**
1631
     * Write the entire project to disk
1632
     */
1633
    private async write(stagingDir: string, files: Map<BscFile, SerializedFile[]>) {
1634
        const programEvent = await this.plugins.emitAsync('beforeWriteProgram', {
346✔
1635
            program: this,
1636
            files: files,
1637
            stagingDir: stagingDir
1638
        });
1639
        //empty the staging directory
1640
        await fsExtra.emptyDir(stagingDir);
346✔
1641

1642
        const serializedFiles = [...files]
346✔
1643
            .map(([, serializedFiles]) => serializedFiles)
701✔
1644
            .flat();
1645

1646
        //write all the files to disk (asynchronously)
1647
        await Promise.all(
346✔
1648
            serializedFiles.map(async (file) => {
1649
                const event = await this.plugins.emitAsync('beforeWriteFile', {
1,079✔
1650
                    program: this,
1651
                    file: file,
1652
                    outputPath: this.getOutputPath(file, stagingDir),
1653
                    processedFiles: new Set<SerializedFile>()
1654
                });
1655

1656
                await this.plugins.emitAsync('writeFile', event);
1,079✔
1657

1658
                await this.plugins.emitAsync('afterWriteFile', event);
1,079✔
1659
            })
1660
        );
1661

1662
        await this.plugins.emitAsync('afterWriteProgram', programEvent);
346✔
1663
    }
1664

1665
    private buildPipeline = new ActionPipeline();
1,790✔
1666

1667
    /**
1668
     * Build the project. This transpiles/transforms/copies all files and moves them to the staging directory
1669
     * @param options the list of options used to build the program
1670
     */
1671
    public async build(options?: ProgramBuildOptions) {
1672
        //run a single build at a time
1673
        await this.buildPipeline.run(async () => {
346✔
1674
            const stagingDir = this.getStagingDir(options?.stagingDir);
346✔
1675

1676
            const event = await this.plugins.emitAsync('beforeBuildProgram', {
346✔
1677
                program: this,
1678
                editor: this.editor,
1679
                files: options?.files ?? Object.values(this.files)
2,076✔
1680
            });
1681

1682
            //prepare the program (and files) for building
1683
            event.files = await this.prepare(event.files);
346✔
1684

1685
            //stage the entire program
1686
            const serializedFilesByFile = await this.serialize(event.files);
346✔
1687

1688
            await this.write(stagingDir, serializedFilesByFile);
346✔
1689

1690
            await this.plugins.emitAsync('afterBuildProgram', event);
346✔
1691

1692
            //undo all edits for the program
1693
            this.editor.undoAll();
346✔
1694
            //undo all edits for each file
1695
            for (const file of event.files) {
346✔
1696
                file.editor.undoAll();
702✔
1697
            }
1698
        });
1699
    }
1700

1701
    /**
1702
     * Find a list of files in the program that have a function with the given name (case INsensitive)
1703
     */
1704
    public findFilesForFunction(functionName: string) {
1705
        const files = [] as BscFile[];
7✔
1706
        const lowerFunctionName = functionName.toLowerCase();
7✔
1707
        //find every file with this function defined
1708
        for (const file of Object.values(this.files)) {
7✔
1709
            if (isBrsFile(file)) {
25✔
1710
                //TODO handle namespace-relative function calls
1711
                //if the file has a function with this name
1712
                // eslint-disable-next-line @typescript-eslint/dot-notation
1713
                if (file['_cachedLookups'].functionStatementMap.get(lowerFunctionName)) {
17✔
1714
                    files.push(file);
2✔
1715
                }
1716
            }
1717
        }
1718
        return files;
7✔
1719
    }
1720

1721
    /**
1722
     * Find a list of files in the program that have a class with the given name (case INsensitive)
1723
     */
1724
    public findFilesForClass(className: string) {
1725
        const files = [] as BscFile[];
7✔
1726
        const lowerClassName = className.toLowerCase();
7✔
1727
        //find every file with this class defined
1728
        for (const file of Object.values(this.files)) {
7✔
1729
            if (isBrsFile(file)) {
25✔
1730
                //TODO handle namespace-relative classes
1731
                //if the file has a function with this name
1732

1733
                // eslint-disable-next-line @typescript-eslint/dot-notation
1734
                if (file['_cachedLookups'].classStatementMap.get(lowerClassName) !== undefined) {
17✔
1735
                    files.push(file);
1✔
1736
                }
1737
            }
1738
        }
1739
        return files;
7✔
1740
    }
1741

1742
    public findFilesForNamespace(name: string) {
1743
        const files = [] as BscFile[];
7✔
1744
        const lowerName = name.toLowerCase();
7✔
1745
        //find every file with this class defined
1746
        for (const file of Object.values(this.files)) {
7✔
1747
            if (isBrsFile(file)) {
25✔
1748

1749
                // eslint-disable-next-line @typescript-eslint/dot-notation
1750
                if (file['_cachedLookups'].namespaceStatements.find((x) => {
17✔
1751
                    const namespaceName = x.name.toLowerCase();
7✔
1752
                    return (
7✔
1753
                        //the namespace name matches exactly
1754
                        namespaceName === lowerName ||
9✔
1755
                        //the full namespace starts with the name (honoring the part boundary)
1756
                        namespaceName.startsWith(lowerName + '.')
1757
                    );
1758
                })) {
1759
                    files.push(file);
6✔
1760
                }
1761
            }
1762
        }
1763

1764
        return files;
7✔
1765
    }
1766

1767
    public findFilesForEnum(name: string) {
1768
        const files = [] as BscFile[];
8✔
1769
        const lowerName = name.toLowerCase();
8✔
1770
        //find every file with this enum defined
1771
        for (const file of Object.values(this.files)) {
8✔
1772
            if (isBrsFile(file)) {
26✔
1773
                // eslint-disable-next-line @typescript-eslint/dot-notation
1774
                if (file['_cachedLookups'].enumStatementMap.get(lowerName)) {
18✔
1775
                    files.push(file);
1✔
1776
                }
1777
            }
1778
        }
1779
        return files;
8✔
1780
    }
1781

1782
    private _manifest: Map<string, string>;
1783

1784
    /**
1785
     * Modify a parsed manifest map by reading `bs_const` and injecting values from `options.manifest.bs_const`
1786
     * @param parsedManifest The manifest map to read from and modify
1787
     */
1788
    private buildBsConstsIntoParsedManifest(parsedManifest: Map<string, string>) {
1789
        // Lift the bs_consts defined in the manifest
1790
        let bsConsts = getBsConst(parsedManifest, false);
15✔
1791

1792
        // Override or delete any bs_consts defined in the bs config
1793
        for (const key in this.options?.manifest?.bs_const) {
15!
1794
            const value = this.options.manifest.bs_const[key];
3✔
1795
            if (value === null) {
3✔
1796
                bsConsts.delete(key);
1✔
1797
            } else {
1798
                bsConsts.set(key, value);
2✔
1799
            }
1800
        }
1801

1802
        // convert the new list of bs consts back into a string for the rest of the down stream systems to use
1803
        let constString = '';
15✔
1804
        for (const [key, value] of bsConsts) {
15✔
1805
            constString += `${constString !== '' ? ';' : ''}${key}=${value.toString()}`;
8✔
1806
        }
1807

1808
        // Set the updated bs_const value
1809
        parsedManifest.set('bs_const', constString);
15✔
1810
    }
1811

1812
    /**
1813
     * Try to find and load the manifest into memory
1814
     * @param manifestFileObj A pointer to a potential manifest file object found during loading
1815
     * @param replaceIfAlreadyLoaded should we overwrite the internal `_manifest` if it already exists
1816
     */
1817
    public loadManifest(manifestFileObj?: FileObj, replaceIfAlreadyLoaded = true) {
1,469✔
1818
        //if we already have a manifest instance, and should not replace...then don't replace
1819
        if (!replaceIfAlreadyLoaded && this._manifest) {
1,475!
UNCOV
1820
            return;
×
1821
        }
1822
        let manifestPath = manifestFileObj
1,475✔
1823
            ? manifestFileObj.src
1,475✔
1824
            : path.join(this.options.rootDir, 'manifest');
1825

1826
        try {
1,475✔
1827
            // we only load this manifest once, so do it sync to improve speed downstream
1828
            const contents = fsExtra.readFileSync(manifestPath, 'utf-8');
1,475✔
1829
            const parsedManifest = parseManifest(contents);
15✔
1830
            this.buildBsConstsIntoParsedManifest(parsedManifest);
15✔
1831
            this._manifest = parsedManifest;
15✔
1832
        } catch (e) {
1833
            this._manifest = new Map();
1,460✔
1834
        }
1835
    }
1836

1837
    /**
1838
     * Get a map of the manifest information
1839
     */
1840
    public getManifest() {
1841
        if (!this._manifest) {
2,298✔
1842
            this.loadManifest();
1,468✔
1843
        }
1844
        return this._manifest;
2,298✔
1845
    }
1846

1847
    public dispose() {
1848
        this.plugins.emit('beforeProgramDispose', { program: this });
1,642✔
1849

1850
        for (let filePath in this.files) {
1,642✔
1851
            this.files[filePath]?.dispose?.();
2,056!
1852
        }
1853
        for (let name in this.scopes) {
1,642✔
1854
            this.scopes[name]?.dispose?.();
3,472!
1855
        }
1856
        this.globalScope?.dispose?.();
1,642!
1857
        this.dependencyGraph?.dispose?.();
1,642!
1858
    }
1859
}
1860

1861
export interface FileTranspileResult {
1862
    srcPath: string;
1863
    destPath: string;
1864
    pkgPath: string;
1865
    code: string;
1866
    map: string;
1867
    typedef: string;
1868
}
1869

1870

1871
class ProvideFileEventInternal<TFile extends BscFile = BscFile> implements ProvideFileEvent<TFile> {
1872
    constructor(
1873
        public program: Program,
2,371✔
1874
        public srcPath: string,
2,371✔
1875
        public destPath: string,
2,371✔
1876
        public data: LazyFileData,
2,371✔
1877
        public fileFactory: FileFactory
2,371✔
1878
    ) {
1879
        this.srcExtension = path.extname(srcPath)?.toLowerCase();
2,371!
1880
    }
1881

1882
    public srcExtension: string;
1883

1884
    public files: TFile[] = [];
2,371✔
1885
}
1886

1887
export interface ProgramBuildOptions {
1888
    /**
1889
     * The directory where the final built files should be placed. This directory will be cleared before running
1890
     */
1891
    stagingDir?: string;
1892
    /**
1893
     * An array of files to build. If omitted, the entire list of files from the program will be used instead.
1894
     * Typically you will want to leave this blank
1895
     */
1896
    files?: BscFile[];
1897
}
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