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

rokucommunity / brighterscript / #12715

14 Jun 2024 08:20PM UTC coverage: 85.629% (-2.3%) from 87.936%
#12715

push

web-flow
Merge 94311dc0a into 42db50190

10808 of 13500 branches covered (80.06%)

Branch coverage included in aggregate %.

6557 of 7163 new or added lines in 96 files covered. (91.54%)

83 existing lines in 17 files now uncovered.

12270 of 13451 relevant lines covered (91.22%)

26531.5 hits per line

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

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

82
        // initialize teh diagnostics Manager
83
        this.diagnostics.logger = this.logger;
1,530✔
84
        this.diagnostics.options = this.options;
1,530✔
85

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

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

92
        this.createGlobalScope();
1,530✔
93

94
        this.fileFactory = new FileFactory(this);
1,530✔
95
    }
96

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

100
    /**
101
     * 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`)
102
     */
103
    public editor = new Editor();
1,530✔
104

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

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

116
        this.populateGlobalSymbolTable();
1,530✔
117

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

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

127

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

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

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

174
        BuiltInInterfaceAdder.getLookupTable = () => this.globalScope.symbolTable;
599,224✔
175

176
        for (const callable of globalCallables) {
1,530✔
177
            this.globalScope.symbolTable.addSymbol(callable.name, { description: callable.shortDescription }, callable.type, SymbolTypeFlag.runtime);
119,340✔
178
        }
179

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

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

195
        for (const nodeData of Object.values(nodes) as SGNodeData[]) {
1,530✔
196
            this.recursivelyAddNodeToSymbolTable(nodeData);
145,350✔
197
        }
198

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

205
    }
206

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

215
    public diagnostics: DiagnosticManager;
216

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

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

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

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

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

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

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

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

259
    public get bslibPrefix() {
260
        if (this.bslibPkgPath === bslibNonAliasedRokuModulesPkgPath) {
634✔
261
            return 'rokucommunity_bslib';
6✔
262
        } else {
263
            return 'bslib';
628✔
264
        }
265
    }
266

267

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

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

285
    protected addScope(scope: Scope) {
286
        this.scopes[scope.name] = scope;
1,678✔
287
    }
288

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

297
    /**
298
     * Get the component with the specified name
299
     */
300
    public getComponent(componentName: string) {
301
        if (componentName) {
1,796✔
302
            //return the first compoment in the list with this name
303
            //(components are ordered in this list by destPath to ensure consistency)
304
            return this.components[componentName.toLowerCase()]?.[0];
1,782✔
305
        } else {
306
            return undefined;
14✔
307
        }
308
    }
309

310
    /**
311
     * Get the sorted names of custom components
312
     */
313
    public getSortedComponentNames() {
314
        const componentNames = Object.keys(this.components);
1,224✔
315
        componentNames.sort((a, b) => {
1,224✔
316
            if (a < b) {
696✔
317
                return -1;
272✔
318
            } else if (b < a) {
424!
319
                return 1;
424✔
320
            }
NEW
321
            return 0;
×
322
        });
323
        return componentNames;
1,224✔
324
    }
325

326
    /**
327
     * Keeps a set of all the components that need to have their types updated during the current validation cycle
328
     */
329
    private componentSymbolsToUpdate = new Set<{ componentKey: string; componentName: string }>();
1,530✔
330

331
    /**
332
     * Register (or replace) the reference to a component in the component map
333
     */
334
    private registerComponent(xmlFile: XmlFile, scope: XmlScope) {
335
        const key = this.getComponentKey(xmlFile);
375✔
336
        if (!this.components[key]) {
375✔
337
            this.components[key] = [];
363✔
338
        }
339
        this.components[key].push({
375✔
340
            file: xmlFile,
341
            scope: scope
342
        });
343
        this.components[key].sort((a, b) => {
375✔
344
            const pathA = a.file.destPath.toLowerCase();
5✔
345
            const pathB = b.file.destPath.toLowerCase();
5✔
346
            if (pathA < pathB) {
5✔
347
                return -1;
1✔
348
            } else if (pathA > pathB) {
4!
349
                return 1;
4✔
350
            }
351
            return 0;
×
352
        });
353
        this.syncComponentDependencyGraph(this.components[key]);
375✔
354
        this.addDeferredComponentTypeSymbolCreation(xmlFile);
375✔
355
    }
356

357
    /**
358
     * Remove the specified component from the components map
359
     */
360
    private unregisterComponent(xmlFile: XmlFile) {
361
        const key = this.getComponentKey(xmlFile);
11✔
362
        const arr = this.components[key] || [];
11!
363
        for (let i = 0; i < arr.length; i++) {
11✔
364
            if (arr[i].file === xmlFile) {
11!
365
                arr.splice(i, 1);
11✔
366
                break;
11✔
367
            }
368
        }
369

370
        this.syncComponentDependencyGraph(arr);
11✔
371
        this.addDeferredComponentTypeSymbolCreation(xmlFile);
11✔
372
    }
373

374
    /**
375
     * Adds a component described in an XML to the set of components that needs to be updated this validation cycle.
376
     * @param xmlFile XML file with <component> tag
377
     */
378
    private addDeferredComponentTypeSymbolCreation(xmlFile: XmlFile) {
379
        this.componentSymbolsToUpdate.add({ componentKey: this.getComponentKey(xmlFile), componentName: xmlFile.componentName?.text });
386✔
380

381
    }
382

383
    private getComponentKey(xmlFile: XmlFile) {
384
        return (xmlFile.componentName?.text ?? xmlFile.pkgPath).toLowerCase();
772✔
385
    }
386

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

413
    /**
414
     * re-attach the dependency graph with a new key for any component who changed
415
     * their position in their own named array (only matters when there are multiple
416
     * components with the same name)
417
     */
418
    private syncComponentDependencyGraph(components: Array<{ file: XmlFile; scope: XmlScope }>) {
419
        //reattach every dependency graph
420
        for (let i = 0; i < components.length; i++) {
386✔
421
            const { file, scope } = components[i];
381✔
422

423
            //attach (or re-attach) the dependencyGraph for every component whose position changed
424
            if (file.dependencyGraphIndex !== i) {
381✔
425
                file.dependencyGraphIndex = i;
377✔
426
                this.dependencyGraph.addOrReplace(file.dependencyGraphKey, file.dependencies);
377✔
427
                file.attachDependencyGraph(this.dependencyGraph);
377✔
428
                scope.attachDependencyGraph(this.dependencyGraph);
377✔
429
            }
430
        }
431
    }
432

433
    /**
434
     * Get a list of all files that are included in the project but are not referenced
435
     * by any scope in the program.
436
     */
437
    public getUnreferencedFiles() {
NEW
438
        let result = [] as BscFile[];
×
UNCOV
439
        for (let filePath in this.files) {
×
UNCOV
440
            let file = this.files[filePath];
×
441
            //is this file part of a scope
UNCOV
442
            if (!this.getFirstScopeForFile(file)) {
×
443
                //no scopes reference this file. add it to the list
UNCOV
444
                result.push(file);
×
445
            }
446
        }
UNCOV
447
        return result;
×
448
    }
449

450
    /**
451
     * Get the list of errors for the entire program.
452
     */
453
    public getDiagnostics() {
454
        return this.diagnostics.getDiagnostics();
1,020✔
455
    }
456

457
    /**
458
     * Determine if the specified file is loaded in this program right now.
459
     * @param filePath the absolute or relative path to the file
460
     * @param normalizePath should the provided path be normalized before use
461
     */
462
    public hasFile(filePath: string, normalizePath = true) {
2,378✔
463
        return !!this.getFile(filePath, normalizePath);
2,378✔
464
    }
465

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

481
    /**
482
     * Return all scopes
483
     */
484
    public getScopes() {
485
        return Object.values(this.scopes);
12✔
486
    }
487

488
    /**
489
     * Find the scope for the specified component
490
     */
491
    public getComponentScope(componentName: string) {
492
        return this.getComponent(componentName)?.scope;
426✔
493
    }
494

495
    /**
496
     * Update internal maps with this file reference
497
     */
498
    private assignFile<T extends BscFile = BscFile>(file: T) {
499
        const fileAddEvent: BeforeFileAddEvent = {
2,199✔
500
            file: file,
501
            program: this
502
        };
503

504
        this.plugins.emit('beforeFileAdd', fileAddEvent);
2,199✔
505

506
        this.files[file.srcPath.toLowerCase()] = file;
2,199✔
507
        this.destMap.set(file.destPath.toLowerCase(), file);
2,199✔
508

509
        this.plugins.emit('afterFileAdd', fileAddEvent);
2,199✔
510

511
        return file;
2,199✔
512
    }
513

514
    /**
515
     * Remove this file from internal maps
516
     */
517
    private unassignFile<T extends BscFile = BscFile>(file: T) {
518
        delete this.files[file.srcPath.toLowerCase()];
152✔
519
        this.destMap.delete(file.destPath.toLowerCase());
152✔
520
        return file;
152✔
521
    }
522

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

540
        let file = this.logger.time(LogLevel.debug, ['Program.setFile()', chalk.green(srcPath)], () => {
2,195✔
541
            //if the file is already loaded, remove it
542
            if (this.hasFile(srcPath)) {
2,195✔
543
                this.removeFile(srcPath, true, true);
136✔
544
            }
545

546
            const data = new LazyFileData(fileData);
2,195✔
547

548
            const event = new ProvideFileEventInternal(this, srcPath, destPath, data, this.fileFactory);
2,195✔
549

550
            this.plugins.emit('beforeProvideFile', event);
2,195✔
551
            this.plugins.emit('provideFile', event);
2,195✔
552
            this.plugins.emit('afterProvideFile', event);
2,195✔
553

554
            //if no files were provided, create a AssetFile to represent it.
555
            if (event.files.length === 0) {
2,195✔
556
                event.files.push(
15✔
557
                    this.fileFactory.AssetFile({
558
                        srcPath: event.srcPath,
559
                        destPath: event.destPath,
560
                        pkgPath: event.destPath,
561
                        data: data
562
                    })
563
                );
564
            }
565

566
            //find the file instance for the srcPath that triggered this action.
567
            const primaryFile = event.files.find(x => x.srcPath === srcPath);
2,195✔
568

569
            if (!primaryFile) {
2,195!
NEW
570
                throw new Error(`No file provided for srcPath '${srcPath}'. Instead, received ${JSON.stringify(event.files.map(x => ({
×
571
                    type: x.type,
572
                    srcPath: x.srcPath,
573
                    destPath: x.destPath
574
                })))}`);
575
            }
576

577
            //link the virtual files to the primary file
578
            this.fileClusters.set(primaryFile.srcPath?.toLowerCase(), event.files);
2,195!
579

580
            for (const file of event.files) {
2,195✔
581
                file.srcPath = s(file.srcPath);
2,199✔
582
                if (file.destPath) {
2,199!
583
                    file.destPath = s`${util.replaceCaseInsensitive(file.destPath, this.options.rootDir, '')}`;
2,199✔
584
                }
585
                if (file.pkgPath) {
2,199✔
586
                    file.pkgPath = s`${util.replaceCaseInsensitive(file.pkgPath, this.options.rootDir, '')}`;
2,195✔
587
                } else {
588
                    file.pkgPath = file.destPath;
4✔
589
                }
590
                file.excludeFromOutput = file.excludeFromOutput === true;
2,199✔
591

592
                //set the dependencyGraph key for every file to its destPath
593
                file.dependencyGraphKey = file.destPath.toLowerCase();
2,199✔
594

595
                this.assignFile(file);
2,199✔
596

597
                //register a callback anytime this file's dependencies change
598
                if (typeof file.onDependenciesChanged === 'function') {
2,199✔
599
                    file.disposables ??= [];
2,176!
600
                    file.disposables.push(
2,176✔
601
                        this.dependencyGraph.onchange(file.dependencyGraphKey, file.onDependenciesChanged.bind(file))
602
                    );
603
                }
604

605
                //register this file (and its dependencies) with the dependency graph
606
                this.dependencyGraph.addOrReplace(file.dependencyGraphKey, file.dependencies ?? []);
2,199✔
607

608
                //if this is a `source` file, add it to the source scope's dependency list
609
                if (this.isSourceBrsFile(file)) {
2,199✔
610
                    this.createSourceScope();
1,478✔
611
                    this.dependencyGraph.addDependency('scope:source', file.dependencyGraphKey);
1,478✔
612
                }
613

614
                //if this is an xml file in the components folder, register it as a component
615
                if (this.isComponentsXmlFile(file)) {
2,199✔
616
                    //create a new scope for this xml file
617
                    let scope = new XmlScope(file, this);
375✔
618
                    this.addScope(scope);
375✔
619

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

623
                    //notify plugins that the scope is created and the component is registered
624
                    this.plugins.emit('afterScopeCreate', {
375✔
625
                        program: this,
626
                        scope: scope
627
                    });
628
                }
629
            }
630

631
            return primaryFile;
2,195✔
632
        });
633
        return file as T;
2,195✔
634
    }
635

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

645
        assert.ok(fileParam, 'fileParam is required');
2,350✔
646

647
        //lift the path vars from the incoming param
648
        if (typeof fileParam === 'string') {
2,350✔
649
            fileParam = this.removePkgPrefix(fileParam);
2,038✔
650
            srcPath = s`${path.resolve(rootDir, fileParam)}`;
2,038✔
651
            destPath = s`${util.replaceCaseInsensitive(srcPath, rootDir, '')}`;
2,038✔
652
        } else {
653
            let param: any = fileParam;
312✔
654

655
            if (param.src) {
312✔
656
                srcPath = s`${param.src}`;
311✔
657
            }
658
            if (param.srcPath) {
312!
UNCOV
659
                srcPath = s`${param.srcPath}`;
×
660
            }
661
            if (param.dest) {
312✔
662
                destPath = s`${this.removePkgPrefix(param.dest)}`;
311✔
663
            }
664
            if (param.pkgPath) {
312!
NEW
665
                destPath = s`${this.removePkgPrefix(param.pkgPath)}`;
×
666
            }
667
        }
668

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

678
        //if destPath isn't set, compute it from the other paths
679
        if (!destPath) {
2,350✔
680
            destPath = s`${util.replaceCaseInsensitive(srcPath, rootDir, '')}`;
1✔
681
        }
682

683
        assert.ok(srcPath, 'fileEntry.src is required');
2,350✔
684
        assert.ok(destPath, 'fileEntry.dest is required');
2,350✔
685

686
        return {
2,350✔
687
            srcPath: srcPath,
688
            //remove leading slash
689
            destPath: destPath.replace(/^[\/\\]+/, '')
690
        };
691
    }
692

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

700
    /**
701
     * Is this file a .brs file found somewhere within the `pkg:/source/` folder?
702
     */
703
    private isSourceBrsFile(file: BscFile) {
704
        return !!/^(pkg:\/)?source[\/\\]/.exec(file.destPath);
2,351✔
705
    }
706

707
    /**
708
     * Is this file a .brs file found somewhere within the `pkg:/source/` folder?
709
     */
710
    private isComponentsXmlFile(file: BscFile): file is XmlFile {
711
        return isXmlFile(file) && !!/^(pkg:\/)?components[\/\\]/.exec(file.destPath);
2,199✔
712
    }
713

714
    /**
715
     * Ensure source scope is created.
716
     * Note: automatically called internally, and no-op if it exists already.
717
     */
718
    public createSourceScope() {
719
        if (!this.scopes.source) {
2,044✔
720
            const sourceScope = new Scope('source', this, 'scope:source');
1,303✔
721
            sourceScope.attachDependencyGraph(this.dependencyGraph);
1,303✔
722
            this.addScope(sourceScope);
1,303✔
723
            this.plugins.emit('afterScopeCreate', {
1,303✔
724
                program: this,
725
                scope: sourceScope
726
            });
727
        }
728
    }
729

730
    /**
731
     * Remove a set of files from the program
732
     * @param srcPaths can be an array of srcPath or destPath strings
733
     * @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
734
     */
735
    public removeFiles(srcPaths: string[], normalizePath = true) {
1✔
736
        for (let srcPath of srcPaths) {
1✔
737
            this.removeFile(srcPath, normalizePath);
1✔
738
        }
739
    }
740

741
    /**
742
     * Remove a file from the program
743
     * @param filePath can be a srcPath, a destPath, or a destPath with leading `pkg:/`
744
     * @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
745
     */
746
    public removeFile(filePath: string, normalizePath = true, keepSymbolInformation = false) {
27✔
747
        this.logger.debug('Program.removeFile()', filePath);
150✔
748
        const paths = this.getPaths(filePath, this.options.rootDir);
150✔
749

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

753
        for (const file of files) {
150✔
754
            //if a file has already been removed, nothing more needs to be done here
755
            if (!file || !this.hasFile(file.srcPath)) {
153✔
756
                continue;
1✔
757
            }
758
            this.diagnostics.clearForFile(file.srcPath);
152✔
759

760
            const event: BeforeFileRemoveEvent = { file: file, program: this };
152✔
761
            this.plugins.emit('beforeFileRemove', event);
152✔
762

763
            //if there is a scope named the same as this file's path, remove it (i.e. xml scopes)
764
            let scope = this.scopes[file.destPath];
152✔
765
            if (scope) {
152✔
766
                const scopeDisposeEvent = {
11✔
767
                    program: this,
768
                    scope: scope
769
                };
770
                this.plugins.emit('beforeScopeDispose', scopeDisposeEvent);
11✔
771
                this.plugins.emit('onScopeDispose', scopeDisposeEvent);
11✔
772
                scope.dispose();
11✔
773
                //notify dependencies of this scope that it has been removed
774
                this.dependencyGraph.remove(scope.dependencyGraphKey!);
11✔
775
                delete this.scopes[file.destPath];
11✔
776
                this.plugins.emit('afterScopeDispose', scopeDisposeEvent);
11✔
777
            }
778
            //remove the file from the program
779
            this.unassignFile(file);
152✔
780

781
            this.dependencyGraph.remove(file.dependencyGraphKey);
152✔
782

783
            //if this is a pkg:/source file, notify the `source` scope that it has changed
784
            if (this.isSourceBrsFile(file)) {
152✔
785
                this.dependencyGraph.removeDependency('scope:source', file.dependencyGraphKey);
126✔
786
            }
787
            if (isBrsFile(file)) {
152✔
788
                if (!keepSymbolInformation) {
135✔
789
                    this.fileSymbolInformation.delete(file.pkgPath);
8✔
790
                }
791
                this.crossScopeValidation.clearResolutionsForFile(file);
135✔
792
            }
793

794
            //if this is a component, remove it from our components map
795
            if (isXmlFile(file)) {
152✔
796
                this.unregisterComponent(file);
11✔
797
            }
798
            //dispose any disposable things on the file
799
            for (const disposable of file?.disposables ?? []) {
152!
800
                disposable();
146✔
801
            }
802
            //dispose file
803
            file?.dispose?.();
152!
804

805
            this.plugins.emit('afterFileRemove', event);
152✔
806
        }
807
    }
808

809
    public crossScopeValidation = new CrossScopeValidator(this);
1,530✔
810

811
    private isFirstValidation = true;
1,530✔
812

813
    /**
814
     * Traverse the entire project, and validate all scopes
815
     */
816
    public validate() {
817
        this.logger.time(LogLevel.log, ['Validating project'], () => {
1,224✔
818
            this.diagnostics.clearForTag(ProgramValidatorDiagnosticsTag);
1,224✔
819
            const programValidateEvent = {
1,224✔
820
                program: this
821
            };
822
            this.plugins.emit('beforeProgramValidate', programValidateEvent);
1,224✔
823
            this.plugins.emit('onProgramValidate', programValidateEvent);
1,224✔
824

825
            const metrics = {
1,224✔
826
                filesChanged: 0,
827
                filesValidated: 0,
828
                fileValidationTime: '',
829
                crossScopeValidationTime: '',
830
                scopesValidated: 0,
831
                totalLinkTime: '',
832
                totalScopeValidationTime: '',
833
                componentValidationTime: ''
834
            };
835

836
            const validationStopwatch = new Stopwatch();
1,224✔
837
            //validate every file
838
            const brsFilesValidated: BrsFile[] = [];
1,224✔
839
            const afterValidateFiles: BscFile[] = [];
1,224✔
840

841
            metrics.fileValidationTime = validationStopwatch.getDurationTextFor(() => {
1,224✔
842
                //sort files by path so we get consistent results
843
                const files = Object.values(this.files).sort(firstBy(x => x.srcPath));
3,456✔
844
                for (const file of files) {
1,224✔
845
                    //for every unvalidated file, validate it
846
                    if (!file.isValidated) {
2,103✔
847
                        const validateFileEvent = {
1,773✔
848
                            program: this,
849
                            file: file
850
                        };
851
                        this.plugins.emit('beforeFileValidate', validateFileEvent);
1,773✔
852
                        //emit an event to allow plugins to contribute to the file validation process
853
                        this.plugins.emit('onFileValidate', validateFileEvent);
1,773✔
854
                        file.isValidated = true;
1,773✔
855
                        if (isBrsFile(file)) {
1,773✔
856
                            brsFilesValidated.push(file);
1,469✔
857
                        }
858
                        afterValidateFiles.push(file);
1,773✔
859
                    }
860
                }
861
                // AfterFileValidate is after all files have been validated
862
                for (const file of afterValidateFiles) {
1,224✔
863
                    const validateFileEvent = {
1,773✔
864
                        program: this,
865
                        file: file
866
                    };
867
                    this.plugins.emit('afterFileValidate', validateFileEvent);
1,773✔
868
                }
869
            }).durationText;
870

871
            metrics.filesChanged = afterValidateFiles.length;
1,224✔
872

873
            // Build component types for any component that changes
874
            this.logger.time(LogLevel.info, ['Build component types'], () => {
1,224✔
875
                for (let { componentKey, componentName } of this.componentSymbolsToUpdate) {
1,224✔
876
                    this.updateComponentSymbolInGlobalScope(componentKey, componentName);
307✔
877
                }
878
                this.componentSymbolsToUpdate.clear();
1,224✔
879
            });
880

881

882
            const changedSymbolsMapArr = brsFilesValidated?.map(f => {
1,224!
883
                if (isBrsFile(f)) {
1,469!
884
                    return f.providedSymbols.changes;
1,469✔
885
                }
NEW
886
                return null;
×
887
            }).filter(x => x);
1,469✔
888

889
            const changedSymbols = new Map<SymbolTypeFlag, Set<string>>();
1,224✔
890
            for (const flag of [SymbolTypeFlag.runtime, SymbolTypeFlag.typetime]) {
1,224✔
891
                const changedSymbolsSetArr = changedSymbolsMapArr.map(symMap => symMap.get(flag));
2,938✔
892
                changedSymbols.set(flag, new Set(...changedSymbolsSetArr));
2,448✔
893
            }
894

895
            const filesToBeValidatedInScopeContext = new Set<BscFile>(afterValidateFiles);
1,224✔
896

897
            metrics.crossScopeValidationTime = validationStopwatch.getDurationTextFor(() => {
1,224✔
898
                const scopesToCheck = this.getScopesForCrossScopeValidation();
1,224✔
899
                this.crossScopeValidation.buildComponentsMap();
1,224✔
900
                this.crossScopeValidation.addDiagnosticsForScopes(scopesToCheck);
1,224✔
901
                const filesToRevalidate = this.crossScopeValidation.getFilesRequiringChangedSymbol(scopesToCheck, changedSymbols);
1,224✔
902
                for (const file of filesToRevalidate) {
1,224✔
903
                    filesToBeValidatedInScopeContext.add(file);
175✔
904
                }
905
            }).durationText;
906

907
            metrics.filesValidated = filesToBeValidatedInScopeContext.size;
1,224✔
908

909
            let linkTime = 0;
1,224✔
910
            let validationTime = 0;
1,224✔
911
            let scopesValidated = 0;
1,224✔
912
            let changedFiles = new Set<BscFile>(afterValidateFiles);
1,224✔
913
            this.logger.time(LogLevel.info, ['Validate all scopes'], () => {
1,224✔
914
                //sort the scope names so we get consistent results
915
                const scopeNames = this.getSortedScopeNames();
1,224✔
916
                for (const file of filesToBeValidatedInScopeContext) {
1,224✔
917
                    if (isBrsFile(file)) {
1,894✔
918
                        file.validationSegmenter.unValidateAllSegments();
1,590✔
919
                    }
920
                }
921
                for (let scopeName of scopeNames) {
1,224✔
922
                    let scope = this.scopes[scopeName];
2,797✔
923
                    const scopeValidated = scope.validate({
2,797✔
924
                        filesToBeValidatedInScopeContext: filesToBeValidatedInScopeContext,
925
                        changedSymbols: changedSymbols,
926
                        changedFiles: changedFiles,
927
                        initialValidation: this.isFirstValidation
928
                    });
929
                    if (scopeValidated) {
2,797✔
930
                        scopesValidated++;
1,525✔
931
                    }
932
                    linkTime += scope.validationMetrics.linkTime;
2,797✔
933
                    validationTime += scope.validationMetrics.validationTime;
2,797✔
934
                }
935
            });
936
            metrics.scopesValidated = scopesValidated;
1,224✔
937
            validationStopwatch.totalMilliseconds = linkTime;
1,224✔
938
            metrics.totalLinkTime = validationStopwatch.getDurationText();
1,224✔
939

940
            validationStopwatch.totalMilliseconds = validationTime;
1,224✔
941
            metrics.totalScopeValidationTime = validationStopwatch.getDurationText();
1,224✔
942

943
            metrics.componentValidationTime = validationStopwatch.getDurationTextFor(() => {
1,224✔
944
                this.detectDuplicateComponentNames();
1,224✔
945
            }).durationText;
946

947
            this.logValidationMetrics(metrics);
1,224✔
948

949
            this.isFirstValidation = false;
1,224✔
950

951
            this.plugins.emit('afterProgramValidate', programValidateEvent);
1,224✔
952
        });
953
    }
954

955
    // eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
956
    private logValidationMetrics(metrics: { [key: string]: number | string }) {
957
        let logs = [] as string[];
1,224✔
958
        for (const key in metrics) {
1,224✔
959
            logs.push(`${key}=${chalk.yellow(metrics[key].toString())}`);
9,792✔
960
        }
961
        this.logger.info(`Validation Metrics: ${logs.join(', ')}`);
1,224✔
962
    }
963

964
    private getScopesForCrossScopeValidation() {
965
        const scopesForCrossScopeValidation = [];
1,224✔
966
        for (let scopeName of this.getSortedScopeNames()) {
1,224✔
967
            let scope = this.scopes[scopeName];
2,797✔
968
            if (this.globalScope !== scope && !scope.isValidated) {
2,797✔
969
                scopesForCrossScopeValidation.push(scope);
1,546✔
970
            }
971
        }
972
        return scopesForCrossScopeValidation;
1,224✔
973
    }
974

975
    /**
976
     * Flag all duplicate component names
977
     */
978
    private detectDuplicateComponentNames() {
979
        const componentsByName = Object.keys(this.files).reduce<Record<string, XmlFile[]>>((map, filePath) => {
1,224✔
980
            const file = this.files[filePath];
2,103✔
981
            //if this is an XmlFile, and it has a valid `componentName` property
982
            if (isXmlFile(file) && file.componentName?.text) {
2,103✔
983
                let lowerName = file.componentName.text.toLowerCase();
436✔
984
                if (!map[lowerName]) {
436✔
985
                    map[lowerName] = [];
433✔
986
                }
987
                map[lowerName].push(file);
436✔
988
            }
989
            return map;
2,103✔
990
        }, {});
991

992
        for (let name in componentsByName) {
1,224✔
993
            const xmlFiles = componentsByName[name];
433✔
994
            //add diagnostics for every duplicate component with this name
995
            if (xmlFiles.length > 1) {
433✔
996
                for (let xmlFile of xmlFiles) {
3✔
997
                    const { componentName } = xmlFile;
6✔
998
                    this.diagnostics.register({
6✔
999
                        ...DiagnosticMessages.duplicateComponentName(componentName.text),
1000
                        range: xmlFile.componentName.location?.range,
18!
1001
                        file: xmlFile,
1002
                        relatedInformation: xmlFiles.filter(x => x !== xmlFile).map(x => {
12✔
1003
                            return {
6✔
1004
                                location: util.createLocationFromRange(
1005
                                    URI.file(xmlFile.srcPath ?? xmlFile.srcPath).toString(),
18!
1006
                                    x.componentName.location?.range
18!
1007
                                ),
1008
                                message: 'Also defined here'
1009
                            };
1010
                        })
1011
                    }, { tags: [ProgramValidatorDiagnosticsTag] });
1012
                }
1013
            }
1014
        }
1015
    }
1016

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

1028
    /**
1029
     * Get the file at the given path
1030
     * @param filePath can be a srcPath or a destPath
1031
     * @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
1032
     */
1033
    public getFile<T extends BscFile>(filePath: string, normalizePath = true) {
10,610✔
1034
        if (typeof filePath !== 'string') {
14,419✔
1035
            return undefined;
3,106✔
1036
            //is the path absolute (or the `virtual:` prefix)
1037
        } else if (/^(?:(?:virtual:[\/\\])|(?:\w:)|(?:[\/\\]))/gmi.exec(filePath)) {
11,313✔
1038
            return this.files[
4,248✔
1039
                (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase()
4,248!
1040
            ] as T;
1041
        } else {
1042
            return this.destMap.get(
7,065✔
1043
                (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase()
7,065✔
1044
            ) as T;
1045
        }
1046
    }
1047

1048
    /**
1049
     * Gets a sorted list of all scopeNames, always beginning with "global", "source", then any others in alphabetical order
1050
     */
1051
    private getSortedScopeNames() {
1052
        return Object.keys(this.scopes).sort((a, b) => {
6,111✔
1053
            if (a === 'global') {
70,059!
NEW
1054
                return -1;
×
1055
            } else if (b === 'global') {
70,059✔
1056
                return 1;
6,304✔
1057
            }
1058
            if (a === 'source') {
63,755✔
1059
                return -1;
132✔
1060
            } else if (b === 'source') {
63,623✔
1061
                return 1;
989✔
1062
            }
1063
            if (a < b) {
62,634✔
1064
                return -1;
23,873✔
1065
            } else if (b < a) {
38,761!
1066
                return 1;
38,761✔
1067
            }
NEW
1068
            return 0;
×
1069
        });
1070
    }
1071

1072
    /**
1073
     * Get a list of all scopes the file is loaded into
1074
     * @param file the file
1075
     */
1076
    public getScopesForFile(file: BscFile | string) {
1077
        const resolvedFile = typeof file === 'string' ? this.getFile(file) : file;
1,328✔
1078

1079
        let result = [] as Scope[];
1,328✔
1080
        if (resolvedFile) {
1,328✔
1081
            const scopeKeys = this.getSortedScopeNames();
1,327✔
1082
            for (let key of scopeKeys) {
1,327✔
1083
                let scope = this.scopes[key];
2,716✔
1084

1085
                if (scope.hasFile(resolvedFile)) {
2,716✔
1086
                    result.push(scope);
735✔
1087
                }
1088
            }
1089
        }
1090
        return result;
1,328✔
1091
    }
1092

1093
    /**
1094
     * Get the first found scope for a file.
1095
     */
1096
    public getFirstScopeForFile(file: BscFile): Scope | undefined {
1097
        const scopeKeys = this.getSortedScopeNames();
2,336✔
1098
        for (let key of scopeKeys) {
2,336✔
1099
            let scope = this.scopes[key];
14,974✔
1100

1101
            if (scope.hasFile(file)) {
14,974✔
1102
                return scope;
2,014✔
1103
            }
1104
        }
1105
    }
1106

1107
    public getStatementsByName(name: string, originFile: BrsFile, namespaceName?: string): FileLink<Statement>[] {
1108
        let results = new Map<Statement, FileLink<Statement>>();
39✔
1109
        const filesSearched = new Set<BrsFile>();
39✔
1110
        let lowerNamespaceName = namespaceName?.toLowerCase();
39✔
1111
        let lowerName = name?.toLowerCase();
39!
1112

1113
        function addToResults(statement: FunctionStatement | MethodStatement, file: BrsFile) {
1114
            let parentNamespaceName = statement.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(originFile.parseMode)?.toLowerCase();
98✔
1115
            if (statement.tokens.name.text.toLowerCase() === lowerName && (!lowerNamespaceName || parentNamespaceName === lowerNamespaceName)) {
98✔
1116
                if (!results.has(statement)) {
36!
1117
                    results.set(statement, { item: statement, file: file as BrsFile });
36✔
1118
                }
1119
            }
1120
        }
1121

1122
        //look through all files in scope for matches
1123
        for (const scope of this.getScopesForFile(originFile)) {
39✔
1124
            for (const file of scope.getAllFiles()) {
39✔
1125
                //skip non-brs files, or files we've already processed
1126
                if (!isBrsFile(file) || filesSearched.has(file)) {
45✔
1127
                    continue;
3✔
1128
                }
1129
                filesSearched.add(file);
42✔
1130

1131
                file.ast.walk(createVisitor({
42✔
1132
                    FunctionStatement: (statement: FunctionStatement) => {
1133
                        addToResults(statement, file);
95✔
1134
                    },
1135
                    MethodStatement: (statement: MethodStatement) => {
1136
                        addToResults(statement, file);
3✔
1137
                    }
1138
                }), {
1139
                    walkMode: WalkMode.visitStatements
1140
                });
1141
            }
1142
        }
1143
        return [...results.values()];
39✔
1144
    }
1145

1146
    public getStatementsForXmlFile(scope: XmlScope, filterName?: string): FileLink<FunctionStatement>[] {
1147
        let results = new Map<Statement, FileLink<FunctionStatement>>();
10✔
1148
        const filesSearched = new Set<BrsFile>();
10✔
1149

1150
        //get all function names for the xml file and parents
1151
        let funcNames = new Set<string>();
10✔
1152
        let currentScope = scope;
10✔
1153
        while (isXmlScope(currentScope)) {
10✔
1154
            for (let name of currentScope.xmlFile.ast.componentElement.interfaceElement?.functions.map((f) => f.name) ?? []) {
16✔
1155
                if (!filterName || name === filterName) {
16!
1156
                    funcNames.add(name);
16✔
1157
                }
1158
            }
1159
            currentScope = currentScope.getParentScope() as XmlScope;
12✔
1160
        }
1161

1162
        //look through all files in scope for matches
1163
        for (const file of scope.getOwnFiles()) {
10✔
1164
            //skip non-brs files, or files we've already processed
1165
            if (!isBrsFile(file) || filesSearched.has(file)) {
20✔
1166
                continue;
10✔
1167
            }
1168
            filesSearched.add(file);
10✔
1169

1170
            file.ast.walk(createVisitor({
10✔
1171
                FunctionStatement: (statement: FunctionStatement) => {
1172
                    if (funcNames.has(statement.tokens.name.text)) {
15!
1173
                        if (!results.has(statement)) {
15!
1174
                            results.set(statement, { item: statement, file: file });
15✔
1175
                        }
1176
                    }
1177
                }
1178
            }), {
1179
                walkMode: WalkMode.visitStatements
1180
            });
1181
        }
1182
        return [...results.values()];
10✔
1183
    }
1184

1185
    /**
1186
     * Find all available completion items at the given position
1187
     * @param filePath can be a srcPath or a destPath
1188
     * @param position the position (line & column) where completions should be found
1189
     */
1190
    public getCompletions(filePath: string, position: Position) {
1191
        let file = this.getFile(filePath);
114✔
1192
        if (!file) {
114!
1193
            return [];
×
1194
        }
1195

1196
        //find the scopes for this file
1197
        let scopes = this.getScopesForFile(file);
114✔
1198

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

1202
        const event: ProvideCompletionsEvent = {
114✔
1203
            program: this,
1204
            file: file,
1205
            scopes: scopes,
1206
            position: position,
1207
            completions: []
1208
        };
1209

1210
        this.plugins.emit('beforeProvideCompletions', event);
114✔
1211

1212
        this.plugins.emit('provideCompletions', event);
114✔
1213

1214
        this.plugins.emit('afterProvideCompletions', event);
114✔
1215

1216
        return event.completions;
114✔
1217
    }
1218

1219
    /**
1220
     * Goes through each file and builds a list of workspace symbols for the program. Used by LanguageServer's onWorkspaceSymbol functionality
1221
     */
1222
    public getWorkspaceSymbols() {
1223
        const event: ProvideWorkspaceSymbolsEvent = {
22✔
1224
            program: this,
1225
            workspaceSymbols: []
1226
        };
1227
        this.plugins.emit('beforeProvideWorkspaceSymbols', event);
22✔
1228
        this.plugins.emit('provideWorkspaceSymbols', event);
22✔
1229
        this.plugins.emit('afterProvideWorkspaceSymbols', event);
22✔
1230
        return event.workspaceSymbols;
22✔
1231
    }
1232

1233
    /**
1234
     * Given a position in a file, if the position is sitting on some type of identifier,
1235
     * go to the definition of that identifier (where this thing was first defined)
1236
     */
1237
    public getDefinition(srcPath: string, position: Position): Location[] {
1238
        let file = this.getFile(srcPath);
18✔
1239
        if (!file) {
18!
1240
            return [];
×
1241
        }
1242

1243
        const event: ProvideDefinitionEvent = {
18✔
1244
            program: this,
1245
            file: file,
1246
            position: position,
1247
            definitions: []
1248
        };
1249

1250
        this.plugins.emit('beforeProvideDefinition', event);
18✔
1251
        this.plugins.emit('provideDefinition', event);
18✔
1252
        this.plugins.emit('afterProvideDefinition', event);
18✔
1253
        return event.definitions;
18✔
1254
    }
1255

1256
    /**
1257
     * Get hover information for a file and position
1258
     */
1259
    public getHover(srcPath: string, position: Position): Hover[] {
1260
        let file = this.getFile(srcPath);
61✔
1261
        let result: Hover[];
1262
        if (file) {
61!
1263
            const event = {
61✔
1264
                program: this,
1265
                file: file,
1266
                position: position,
1267
                scopes: this.getScopesForFile(file),
1268
                hovers: []
1269
            } as ProvideHoverEvent;
1270
            this.plugins.emit('beforeProvideHover', event);
61✔
1271
            this.plugins.emit('provideHover', event);
61✔
1272
            this.plugins.emit('afterProvideHover', event);
61✔
1273
            result = event.hovers;
61✔
1274
        }
1275

1276
        return result ?? [];
61!
1277
    }
1278

1279
    /**
1280
     * Get full list of document symbols for a file
1281
     * @param srcPath path to the file
1282
     */
1283
    public getDocumentSymbols(srcPath: string): DocumentSymbol[] | undefined {
1284
        let file = this.getFile(srcPath);
18✔
1285
        if (file) {
18!
1286
            const event: ProvideDocumentSymbolsEvent = {
18✔
1287
                program: this,
1288
                file: file,
1289
                documentSymbols: []
1290
            };
1291
            this.plugins.emit('beforeProvideDocumentSymbols', event);
18✔
1292
            this.plugins.emit('provideDocumentSymbols', event);
18✔
1293
            this.plugins.emit('afterProvideDocumentSymbols', event);
18✔
1294
            return event.documentSymbols;
18✔
1295
        } else {
1296
            return undefined;
×
1297
        }
1298
    }
1299

1300
    /**
1301
     * Compute code actions for the given file and range
1302
     */
1303
    public getCodeActions(srcPath: string, range: Range) {
1304
        const codeActions = [] as CodeAction[];
11✔
1305
        const file = this.getFile(srcPath);
11✔
1306
        if (file) {
11✔
1307
            const diagnostics = this
10✔
1308
                //get all current diagnostics (filtered by diagnostic filters)
1309
                .getDiagnostics()
1310
                //only keep diagnostics related to this file
1311
                .filter(x => x.file === file)
23✔
1312
                //only keep diagnostics that touch this range
1313
                .filter(x => util.rangesIntersectOrTouch(x.range, range));
12✔
1314

1315
            const scopes = this.getScopesForFile(file);
10✔
1316

1317
            this.plugins.emit('onGetCodeActions', {
10✔
1318
                program: this,
1319
                file: file,
1320
                range: range,
1321
                diagnostics: diagnostics,
1322
                scopes: scopes,
1323
                codeActions: codeActions
1324
            });
1325
        }
1326
        return codeActions;
11✔
1327
    }
1328

1329
    /**
1330
     * Get semantic tokens for the specified file
1331
     */
1332
    public getSemanticTokens(srcPath: string): SemanticToken[] | undefined {
1333
        const file = this.getFile(srcPath);
22✔
1334
        if (file) {
22!
1335
            const result = [] as SemanticToken[];
22✔
1336
            this.plugins.emit('onGetSemanticTokens', {
22✔
1337
                program: this,
1338
                file: file,
1339
                scopes: this.getScopesForFile(file),
1340
                semanticTokens: result
1341
            });
1342
            return result;
22✔
1343
        }
1344
    }
1345

1346
    public getSignatureHelp(filepath: string, position: Position): SignatureInfoObj[] {
1347
        let file: BrsFile = this.getFile(filepath);
185✔
1348
        if (!file || !isBrsFile(file)) {
185✔
1349
            return [];
3✔
1350
        }
1351
        let callExpressionInfo = new CallExpressionInfo(file, position);
182✔
1352
        let signatureHelpUtil = new SignatureHelpUtil();
182✔
1353
        return signatureHelpUtil.getSignatureHelpItems(callExpressionInfo);
182✔
1354
    }
1355

1356
    public getReferences(srcPath: string, position: Position): Location[] {
1357
        //find the file
1358
        let file = this.getFile(srcPath);
4✔
1359

1360
        const event: ProvideReferencesEvent = {
4✔
1361
            program: this,
1362
            file: file,
1363
            position: position,
1364
            references: []
1365
        };
1366

1367
        this.plugins.emit('beforeProvideReferences', event);
4✔
1368
        this.plugins.emit('provideReferences', event);
4✔
1369
        this.plugins.emit('afterProvideReferences', event);
4✔
1370

1371
        return event.references;
4✔
1372
    }
1373

1374
    /**
1375
     * Transpile a single file and get the result as a string.
1376
     * This does not write anything to the file system.
1377
     *
1378
     * This should only be called by `LanguageServer`.
1379
     * Internal usage should call `_getTranspiledFileContents` instead.
1380
     * @param filePath can be a srcPath or a destPath
1381
     */
1382
    public async getTranspiledFileContents(filePath: string): Promise<FileTranspileResult> {
1383
        const file = this.getFile(filePath);
270✔
1384

1385
        return this.getTranspiledFileContentsPipeline.run(async () => {
270✔
1386

1387
            const result = {
270✔
1388
                destPath: file.destPath,
1389
                pkgPath: file.pkgPath,
1390
                srcPath: file.srcPath
1391
            } as FileTranspileResult;
1392

1393
            const expectedPkgPath = file.pkgPath.toLowerCase();
270✔
1394
            const expectedMapPath = `${expectedPkgPath}.map`;
270✔
1395
            const expectedTypedefPkgPath = expectedPkgPath.replace(/\.brs$/i, '.d.bs');
270✔
1396

1397
            //add a temporary plugin to tap into the file writing process
1398
            const plugin = this.plugins.addFirst({
270✔
1399
                name: 'getTranspiledFileContents',
1400
                beforeWriteFile: (event) => {
1401
                    const pkgPath = event.file.pkgPath.toLowerCase();
828✔
1402
                    switch (pkgPath) {
828✔
1403
                        //this is the actual transpiled file
1404
                        case expectedPkgPath:
828✔
1405
                            result.code = event.file.data.toString();
270✔
1406
                            break;
270✔
1407
                        //this is the sourcemap
1408
                        case expectedMapPath:
1409
                            result.map = event.file.data.toString();
136✔
1410
                            break;
136✔
1411
                        //this is the typedef
1412
                        case expectedTypedefPkgPath:
1413
                            result.typedef = event.file.data.toString();
8✔
1414
                            break;
8✔
1415
                        default:
1416
                        //no idea what this file is. just ignore it
1417
                    }
1418
                    //mark every file as processed so it they don't get written to the output directory
1419
                    event.processedFiles.add(event.file);
828✔
1420
                }
1421
            });
1422

1423
            try {
270✔
1424
                //now that the plugin has been registered, run the build with just this file
1425
                await this.build({
270✔
1426
                    files: [file]
1427
                });
1428
            } finally {
1429
                this.plugins.remove(plugin);
270✔
1430
            }
1431
            return result;
270✔
1432
        });
1433
    }
1434
    private getTranspiledFileContentsPipeline = new ActionPipeline();
1,530✔
1435

1436
    /**
1437
     * Get the absolute output path for a file
1438
     */
1439
    private getOutputPath(file: { pkgPath?: string }, stagingDir = this.getStagingDir()) {
×
1440
        return s`${stagingDir}/${file.pkgPath}`;
1,551✔
1441
    }
1442

1443
    private getStagingDir(stagingDir?: string) {
1444
        let result = stagingDir ?? this.options.stagingDir ?? this.options.stagingDir;
615✔
1445
        if (!result) {
615✔
1446
            result = rokuDeploy.getOptions(this.options as any).stagingDir;
439✔
1447
        }
1448
        result = s`${path.resolve(this.options.cwd ?? process.cwd(), result ?? '/')}`;
615!
1449
        return result;
615✔
1450
    }
1451

1452
    /**
1453
     * Prepare the program for building
1454
     * @param files the list of files that should be prepared
1455
     */
1456
    private async prepare(files: BscFile[]) {
1457
        const programEvent = {
308✔
1458
            program: this,
1459
            editor: this.editor,
1460
            files: files
1461
        } as PrepareProgramEvent;
1462

1463
        //assign an editor to every file
1464
        for (const file of files) {
308✔
1465
            //if the file doesn't have an editor yet, assign one now
1466
            if (!file.editor) {
624✔
1467
                file.editor = new Editor();
577✔
1468
            }
1469
        }
1470

1471
        files.sort((a, b) => {
308✔
1472
            if (a.pkgPath < b.pkgPath) {
330✔
1473
                return -1;
275✔
1474
            } else if (a.pkgPath > b.pkgPath) {
55!
1475
                return 1;
55✔
1476
            } else {
NEW
1477
                return 1;
×
1478
            }
1479
        });
1480

1481
        await this.plugins.emitAsync('beforePrepareProgram', programEvent);
308✔
1482
        await this.plugins.emitAsync('prepareProgram', programEvent);
308✔
1483

1484
        const stagingDir = this.getStagingDir();
308✔
1485

1486
        const entries: TranspileObj[] = [];
308✔
1487

1488
        for (const file of files) {
308✔
1489
            //if the file doesn't have an editor yet, assign one now
1490
            if (!file.editor) {
624!
NEW
1491
                file.editor = new Editor();
×
1492
            }
1493
            const event = {
624✔
1494
                program: this,
1495
                file: file,
1496
                editor: file.editor,
1497
                outputPath: this.getOutputPath(file, stagingDir)
1498
            } as PrepareFileEvent & { outputPath: string };
1499

1500
            await this.plugins.emitAsync('beforePrepareFile', event);
624✔
1501
            await this.plugins.emitAsync('prepareFile', event);
624✔
1502
            await this.plugins.emitAsync('afterPrepareFile', event);
624✔
1503

1504
            //TODO remove this in v1
1505
            entries.push(event);
624✔
1506
        }
1507

1508
        await this.plugins.emitAsync('afterPrepareProgram', programEvent);
308✔
1509
        return files;
308✔
1510
    }
1511

1512
    /**
1513
     * Generate the contents of every file
1514
     */
1515
    private async serialize(files: BscFile[]) {
1516

1517
        const allFiles = new Map<BscFile, SerializedFile[]>();
307✔
1518

1519
        //exclude prunable files if that option is enabled
1520
        if (this.options.pruneEmptyCodeFiles === true) {
307✔
1521
            files = files.filter(x => x.canBePruned !== true);
9✔
1522
        }
1523

1524
        const serializeProgramEvent = await this.plugins.emitAsync('beforeSerializeProgram', {
307✔
1525
            program: this,
1526
            files: files,
1527
            result: allFiles
1528
        });
1529
        await this.plugins.emitAsync('onSerializeProgram', {
307✔
1530
            program: this,
1531
            files: files,
1532
            result: allFiles
1533
        });
1534

1535
        //sort the entries to make transpiling more deterministic
1536
        files = serializeProgramEvent.files.sort((a, b) => {
307✔
1537
            return a.srcPath < b.srcPath ? -1 : 1;
323✔
1538
        });
1539

1540
        // serialize each file
1541
        for (const file of files) {
307✔
1542
            const event = {
621✔
1543
                program: this,
1544
                file: file,
1545
                result: allFiles
1546
            };
1547
            await this.plugins.emitAsync('beforeSerializeFile', event);
621✔
1548
            await this.plugins.emitAsync('serializeFile', event);
621✔
1549
            await this.plugins.emitAsync('afterSerializeFile', event);
621✔
1550
        }
1551

1552
        this.plugins.emit('afterSerializeProgram', {
307✔
1553
            program: this,
1554
            files: files,
1555
            result: allFiles
1556
        });
1557

1558
        return allFiles;
307✔
1559
    }
1560

1561
    /**
1562
     * Write the entire project to disk
1563
     */
1564
    private async write(stagingDir: string, files: Map<BscFile, SerializedFile[]>) {
1565
        const programEvent = await this.plugins.emitAsync('beforeWriteProgram', {
307✔
1566
            program: this,
1567
            files: files,
1568
            stagingDir: stagingDir
1569
        });
1570
        //empty the staging directory
1571
        await fsExtra.emptyDir(stagingDir);
307✔
1572

1573
        const serializedFiles = [...files]
307✔
1574
            .map(([, serializedFiles]) => serializedFiles)
621✔
1575
            .flat();
1576

1577
        //write all the files to disk (asynchronously)
1578
        await Promise.all(
307✔
1579
            serializedFiles.map(async (file) => {
1580
                const event = await this.plugins.emitAsync('beforeWriteFile', {
927✔
1581
                    program: this,
1582
                    file: file,
1583
                    outputPath: this.getOutputPath(file, stagingDir),
1584
                    processedFiles: new Set<SerializedFile>()
1585
                });
1586

1587
                await this.plugins.emitAsync('writeFile', event);
927✔
1588

1589
                await this.plugins.emitAsync('afterWriteFile', event);
927✔
1590
            })
1591
        );
1592

1593
        await this.plugins.emitAsync('afterWriteProgram', programEvent);
307✔
1594
    }
1595

1596
    private buildPipeline = new ActionPipeline();
1,530✔
1597

1598
    /**
1599
     * Build the project. This transpiles/transforms/copies all files and moves them to the staging directory
1600
     * @param options the list of options used to build the program
1601
     */
1602
    public async build(options?: ProgramBuildOptions) {
1603
        //run a single build at a time
1604
        await this.buildPipeline.run(async () => {
307✔
1605
            const stagingDir = this.getStagingDir(options?.stagingDir);
307✔
1606

1607
            const event = await this.plugins.emitAsync('beforeBuildProgram', {
307✔
1608
                program: this,
1609
                editor: this.editor,
1610
                files: options?.files ?? Object.values(this.files)
1,842✔
1611
            });
1612

1613
            //prepare the program (and files) for building
1614
            event.files = await this.prepare(event.files);
307✔
1615

1616
            //stage the entire program
1617
            const serializedFilesByFile = await this.serialize(event.files);
307✔
1618

1619
            await this.write(stagingDir, serializedFilesByFile);
307✔
1620

1621
            await this.plugins.emitAsync('afterBuildProgram', event);
307✔
1622

1623
            //undo all edits for the program
1624
            this.editor.undoAll();
307✔
1625
            //undo all edits for each file
1626
            for (const file of event.files) {
307✔
1627
                file.editor.undoAll();
622✔
1628
            }
1629
        });
1630
    }
1631

1632
    /**
1633
     * Find a list of files in the program that have a function with the given name (case INsensitive)
1634
     */
1635
    public findFilesForFunction(functionName: string) {
1636
        const files = [] as BscFile[];
7✔
1637
        const lowerFunctionName = functionName.toLowerCase();
7✔
1638
        //find every file with this function defined
1639
        for (const file of Object.values(this.files)) {
7✔
1640
            if (isBrsFile(file)) {
25✔
1641
                //TODO handle namespace-relative function calls
1642
                //if the file has a function with this name
1643
                // eslint-disable-next-line @typescript-eslint/dot-notation
1644
                if (file['_cachedLookups'].functionStatementMap.get(lowerFunctionName)) {
17✔
1645
                    files.push(file);
2✔
1646
                }
1647
            }
1648
        }
1649
        return files;
7✔
1650
    }
1651

1652
    /**
1653
     * Find a list of files in the program that have a class with the given name (case INsensitive)
1654
     */
1655
    public findFilesForClass(className: string) {
1656
        const files = [] as BscFile[];
7✔
1657
        const lowerClassName = className.toLowerCase();
7✔
1658
        //find every file with this class defined
1659
        for (const file of Object.values(this.files)) {
7✔
1660
            if (isBrsFile(file)) {
25✔
1661
                //TODO handle namespace-relative classes
1662
                //if the file has a function with this name
1663

1664
                // eslint-disable-next-line @typescript-eslint/dot-notation
1665
                if (file['_cachedLookups'].classStatementMap.get(lowerClassName) !== undefined) {
17✔
1666
                    files.push(file);
1✔
1667
                }
1668
            }
1669
        }
1670
        return files;
7✔
1671
    }
1672

1673
    public findFilesForNamespace(name: string) {
1674
        const files = [] as BscFile[];
7✔
1675
        const lowerName = name.toLowerCase();
7✔
1676
        //find every file with this class defined
1677
        for (const file of Object.values(this.files)) {
7✔
1678
            if (isBrsFile(file)) {
25✔
1679

1680
                // eslint-disable-next-line @typescript-eslint/dot-notation
1681
                if (file['_cachedLookups'].namespaceStatements.find((x) => {
17✔
1682
                    const namespaceName = x.name.toLowerCase();
7✔
1683
                    return (
7✔
1684
                        //the namespace name matches exactly
1685
                        namespaceName === lowerName ||
9✔
1686
                        //the full namespace starts with the name (honoring the part boundary)
1687
                        namespaceName.startsWith(lowerName + '.')
1688
                    );
1689
                })) {
1690
                    files.push(file);
6✔
1691
                }
1692
            }
1693
        }
1694

1695
        return files;
7✔
1696
    }
1697

1698
    public findFilesForEnum(name: string) {
1699
        const files = [] as BscFile[];
8✔
1700
        const lowerName = name.toLowerCase();
8✔
1701
        //find every file with this enum defined
1702
        for (const file of Object.values(this.files)) {
8✔
1703
            if (isBrsFile(file)) {
26✔
1704
                // eslint-disable-next-line @typescript-eslint/dot-notation
1705
                if (file['_cachedLookups'].enumStatementMap.get(lowerName)) {
18✔
1706
                    files.push(file);
1✔
1707
                }
1708
            }
1709
        }
1710
        return files;
8✔
1711
    }
1712

1713
    private _manifest: Map<string, string>;
1714

1715
    /**
1716
     * Modify a parsed manifest map by reading `bs_const` and injecting values from `options.manifest.bs_const`
1717
     * @param parsedManifest The manifest map to read from and modify
1718
     */
1719
    private buildBsConstsIntoParsedManifest(parsedManifest: Map<string, string>) {
1720
        // Lift the bs_consts defined in the manifest
1721
        let bsConsts = getBsConst(parsedManifest, false);
13✔
1722

1723
        // Override or delete any bs_consts defined in the bs config
1724
        for (const key in this.options?.manifest?.bs_const) {
13!
1725
            const value = this.options.manifest.bs_const[key];
3✔
1726
            if (value === null) {
3✔
1727
                bsConsts.delete(key);
1✔
1728
            } else {
1729
                bsConsts.set(key, value);
2✔
1730
            }
1731
        }
1732

1733
        // convert the new list of bs consts back into a string for the rest of the down stream systems to use
1734
        let constString = '';
13✔
1735
        for (const [key, value] of bsConsts) {
13✔
1736
            constString += `${constString !== '' ? ';' : ''}${key}=${value.toString()}`;
6✔
1737
        }
1738

1739
        // Set the updated bs_const value
1740
        parsedManifest.set('bs_const', constString);
13✔
1741
    }
1742

1743
    /**
1744
     * Try to find and load the manifest into memory
1745
     * @param manifestFileObj A pointer to a potential manifest file object found during loading
1746
     * @param replaceIfAlreadyLoaded should we overwrite the internal `_manifest` if it already exists
1747
     */
1748
    public loadManifest(manifestFileObj?: FileObj, replaceIfAlreadyLoaded = true) {
1,351✔
1749
        //if we already have a manifest instance, and should not replace...then don't replace
1750
        if (!replaceIfAlreadyLoaded && this._manifest) {
1,357!
1751
            return;
×
1752
        }
1753
        let manifestPath = manifestFileObj
1,357✔
1754
            ? manifestFileObj.src
1,357✔
1755
            : path.join(this.options.rootDir, 'manifest');
1756

1757
        try {
1,357✔
1758
            // we only load this manifest once, so do it sync to improve speed downstream
1759
            const contents = fsExtra.readFileSync(manifestPath, 'utf-8');
1,357✔
1760
            const parsedManifest = parseManifest(contents);
13✔
1761
            this.buildBsConstsIntoParsedManifest(parsedManifest);
13✔
1762
            this._manifest = parsedManifest;
13✔
1763
        } catch (e) {
1764
            this._manifest = new Map();
1,344✔
1765
        }
1766
    }
1767

1768
    /**
1769
     * Get a map of the manifest information
1770
     */
1771
    public getManifest() {
1772
        if (!this._manifest) {
2,113✔
1773
            this.loadManifest();
1,350✔
1774
        }
1775
        return this._manifest;
2,113✔
1776
    }
1777

1778
    public dispose() {
1779
        this.plugins.emit('beforeProgramDispose', { program: this });
1,404✔
1780

1781
        for (let filePath in this.files) {
1,404✔
1782
            this.files[filePath]?.dispose?.();
1,902!
1783
        }
1784
        for (let name in this.scopes) {
1,404✔
1785
            this.scopes[name]?.dispose?.();
2,977!
1786
        }
1787
        this.globalScope?.dispose?.();
1,404!
1788
        this.dependencyGraph?.dispose?.();
1,404!
1789
    }
1790
}
1791

1792
export interface FileTranspileResult {
1793
    srcPath: string;
1794
    destPath: string;
1795
    pkgPath: string;
1796
    code: string;
1797
    map: string;
1798
    typedef: string;
1799
}
1800

1801

1802
class ProvideFileEventInternal<TFile extends BscFile = BscFile> implements ProvideFileEvent<TFile> {
1803
    constructor(
1804
        public program: Program,
2,195✔
1805
        public srcPath: string,
2,195✔
1806
        public destPath: string,
2,195✔
1807
        public data: LazyFileData,
2,195✔
1808
        public fileFactory: FileFactory
2,195✔
1809
    ) {
1810
        this.srcExtension = path.extname(srcPath)?.toLowerCase();
2,195!
1811
    }
1812

1813
    public srcExtension: string;
1814

1815
    public files: TFile[] = [];
2,195✔
1816
}
1817

1818
export interface ProgramBuildOptions {
1819
    /**
1820
     * The directory where the final built files should be placed. This directory will be cleared before running
1821
     */
1822
    stagingDir?: string;
1823
    /**
1824
     * An array of files to build. If omitted, the entire list of files from the program will be used instead.
1825
     * Typically you will want to leave this blank
1826
     */
1827
    files?: BscFile[];
1828
}
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