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

rokucommunity / brighterscript / #12851

25 Jul 2024 01:32PM UTC coverage: 86.202% (-0.003%) from 86.205%
#12851

push

web-flow
Merge 242a1aefa into 5f3ffa3fa

10593 of 13078 branches covered (81.0%)

Branch coverage included in aggregate %.

91 of 97 new or added lines in 15 files covered. (93.81%)

288 existing lines in 16 files now uncovered.

12292 of 13470 relevant lines covered (91.25%)

26625.31 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

128

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

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

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

175
        BuiltInInterfaceAdder.getLookupTable = () => this.globalScope.symbolTable;
601,179✔
176

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

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

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

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

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

206
    }
207

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

216
    public diagnostics: DiagnosticManager;
217

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

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

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

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

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

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

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

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

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

268

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

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

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

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

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

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

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

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

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

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

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

382
    }
383

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

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

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

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

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

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

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

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

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

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

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

505
        this.plugins.emit('beforeFileAdd', fileAddEvent);
2,207✔
506

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

510
        this.plugins.emit('afterFileAdd', fileAddEvent);
2,207✔
511

512
        return file;
2,207✔
513
    }
514

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

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

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

547
            const data = new LazyFileData(fileData);
2,203✔
548

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

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

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

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

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

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

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

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

596
                this.assignFile(file);
2,207✔
597

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

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

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

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

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

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

632
            return primaryFile;
2,203✔
633
        });
634
        return file as T;
2,203✔
635
    }
636

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

646
        assert.ok(fileParam, 'fileParam is required');
2,358✔
647

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

810
    public crossScopeValidation = new CrossScopeValidator(this);
1,535✔
811

812
    private isFirstValidation = true;
1,535✔
813

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

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

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

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

872
            metrics.filesChanged = afterValidateFiles.length;
1,227✔
873

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

882

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

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

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

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

908
            metrics.filesValidated = filesToBeValidatedInScopeContext.size;
1,227✔
909

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

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

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

948
            this.logValidationMetrics(metrics);
1,227✔
949

950
            this.isFirstValidation = false;
1,227✔
951

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

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

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

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

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

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

1025
    /**
1026
     * Get the file at the given path
1027
     * @param filePath can be a srcPath or a destPath
1028
     * @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
1029
     */
1030
    public getFile<T extends BscFile>(filePath: string, normalizePath = true) {
11,314✔
1031
        if (typeof filePath !== 'string') {
15,133✔
1032
            return undefined;
3,112✔
1033
            //is the path absolute (or the `virtual:` prefix)
1034
        } else if (/^(?:(?:virtual:[\/\\])|(?:\w:)|(?:[\/\\]))/gmi.exec(filePath)) {
12,021✔
1035
            return this.files[
4,266✔
1036
                (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase()
4,266!
1037
            ] as T;
1038
        } else if (util.isUriLike(filePath)) {
7,755✔
1039
            const path = URI.parse(filePath).fsPath;
668✔
1040
            return this.files[
668✔
1041
                (normalizePath ? util.standardizePath(path) : path).toLowerCase()
668!
1042
            ] as T;
1043
        } else {
1044
            return this.destMap.get(
7,087✔
1045
                (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase()
7,087✔
1046
            ) as T;
1047
        }
1048
    }
1049

1050
    /**
1051
     * Gets a sorted list of all scopeNames, always beginning with "global", "source", then any others in alphabetical order
1052
     */
1053
    private getSortedScopeNames() {
1054
        return Object.keys(this.scopes).sort((a, b) => {
6,237✔
1055
            if (a === 'global') {
70,192!
UNCOV
1056
                return -1;
×
1057
            } else if (b === 'global') {
70,192✔
1058
                return 1;
6,425✔
1059
            }
1060
            if (a === 'source') {
63,767✔
1061
                return -1;
122✔
1062
            } else if (b === 'source') {
63,645✔
1063
                return 1;
1,007✔
1064
            }
1065
            if (a < b) {
62,638✔
1066
                return -1;
23,875✔
1067
            } else if (b < a) {
38,763!
1068
                return 1;
38,763✔
1069
            }
UNCOV
1070
            return 0;
×
1071
        });
1072
    }
1073

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

1081
        let result = [] as Scope[];
1,441✔
1082
        if (resolvedFile) {
1,441✔
1083
            const scopeKeys = this.getSortedScopeNames();
1,440✔
1084
            for (let key of scopeKeys) {
1,440✔
1085
                let scope = this.scopes[key];
2,954✔
1086

1087
                if (scope.hasFile(resolvedFile)) {
2,954✔
1088
                    result.push(scope);
845✔
1089
                }
1090
            }
1091
        }
1092
        return result;
1,441✔
1093
    }
1094

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

1103
            if (scope.hasFile(file)) {
14,988✔
1104
                return scope;
2,019✔
1105
            }
1106
        }
1107
    }
1108

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

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

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

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

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

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

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

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

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

1198
        //find the scopes for this file
1199
        let scopes = this.getScopesForFile(file);
115✔
1200

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

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

1212
        this.plugins.emit('beforeProvideCompletions', event);
115✔
1213

1214
        this.plugins.emit('provideCompletions', event);
115✔
1215

1216
        this.plugins.emit('afterProvideCompletions', event);
115✔
1217

1218
        return event.completions;
115✔
1219
    }
1220

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

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

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

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

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

1278
        return result ?? [];
61!
1279
    }
1280

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

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

1318
            const scopes = this.getScopesForFile(file);
10✔
1319

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

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

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

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

1363
        const event: ProvideReferencesEvent = {
4✔
1364
            program: this,
1365
            file: file,
1366
            position: position,
1367
            references: []
1368
        };
1369

1370
        this.plugins.emit('beforeProvideReferences', event);
4✔
1371
        this.plugins.emit('provideReferences', event);
4✔
1372
        this.plugins.emit('afterProvideReferences', event);
4✔
1373

1374
        return event.references;
4✔
1375
    }
1376

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

1388
        return this.getTranspiledFileContentsPipeline.run(async () => {
272✔
1389

1390
            const result = {
272✔
1391
                destPath: file.destPath,
1392
                pkgPath: file.pkgPath,
1393
                srcPath: file.srcPath
1394
            } as FileTranspileResult;
1395

1396
            const expectedPkgPath = file.pkgPath.toLowerCase();
272✔
1397
            const expectedMapPath = `${expectedPkgPath}.map`;
272✔
1398
            const expectedTypedefPkgPath = expectedPkgPath.replace(/\.brs$/i, '.d.bs');
272✔
1399

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

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

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

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

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

1466
        //assign an editor to every file
1467
        for (const file of files) {
310✔
1468
            //if the file doesn't have an editor yet, assign one now
1469
            if (!file.editor) {
628✔
1470
                file.editor = new Editor();
581✔
1471
            }
1472
        }
1473

1474
        files.sort((a, b) => {
310✔
1475
            if (a.pkgPath < b.pkgPath) {
332✔
1476
                return -1;
277✔
1477
            } else if (a.pkgPath > b.pkgPath) {
55!
1478
                return 1;
55✔
1479
            } else {
UNCOV
1480
                return 1;
×
1481
            }
1482
        });
1483

1484
        await this.plugins.emitAsync('beforePrepareProgram', programEvent);
310✔
1485
        await this.plugins.emitAsync('prepareProgram', programEvent);
310✔
1486

1487
        const stagingDir = this.getStagingDir();
310✔
1488

1489
        const entries: TranspileObj[] = [];
310✔
1490

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

1503
            await this.plugins.emitAsync('beforePrepareFile', event);
628✔
1504
            await this.plugins.emitAsync('prepareFile', event);
628✔
1505
            await this.plugins.emitAsync('afterPrepareFile', event);
628✔
1506

1507
            //TODO remove this in v1
1508
            entries.push(event);
628✔
1509
        }
1510

1511
        await this.plugins.emitAsync('afterPrepareProgram', programEvent);
310✔
1512
        return files;
310✔
1513
    }
1514

1515
    /**
1516
     * Generate the contents of every file
1517
     */
1518
    private async serialize(files: BscFile[]) {
1519

1520
        const allFiles = new Map<BscFile, SerializedFile[]>();
309✔
1521

1522
        //exclude prunable files if that option is enabled
1523
        if (this.options.pruneEmptyCodeFiles === true) {
309✔
1524
            files = files.filter(x => x.canBePruned !== true);
9✔
1525
        }
1526

1527
        const serializeProgramEvent = await this.plugins.emitAsync('beforeSerializeProgram', {
309✔
1528
            program: this,
1529
            files: files,
1530
            result: allFiles
1531
        });
1532
        await this.plugins.emitAsync('onSerializeProgram', {
309✔
1533
            program: this,
1534
            files: files,
1535
            result: allFiles
1536
        });
1537

1538
        //sort the entries to make transpiling more deterministic
1539
        files = serializeProgramEvent.files.sort((a, b) => {
309✔
1540
            return a.srcPath < b.srcPath ? -1 : 1;
325✔
1541
        });
1542

1543
        // serialize each file
1544
        for (const file of files) {
309✔
1545
            const event = {
625✔
1546
                program: this,
1547
                file: file,
1548
                result: allFiles
1549
            };
1550
            await this.plugins.emitAsync('beforeSerializeFile', event);
625✔
1551
            await this.plugins.emitAsync('serializeFile', event);
625✔
1552
            await this.plugins.emitAsync('afterSerializeFile', event);
625✔
1553
        }
1554

1555
        this.plugins.emit('afterSerializeProgram', {
309✔
1556
            program: this,
1557
            files: files,
1558
            result: allFiles
1559
        });
1560

1561
        return allFiles;
309✔
1562
    }
1563

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

1576
        const serializedFiles = [...files]
309✔
1577
            .map(([, serializedFiles]) => serializedFiles)
625✔
1578
            .flat();
1579

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

1590
                await this.plugins.emitAsync('writeFile', event);
931✔
1591

1592
                await this.plugins.emitAsync('afterWriteFile', event);
931✔
1593
            })
1594
        );
1595

1596
        await this.plugins.emitAsync('afterWriteProgram', programEvent);
309✔
1597
    }
1598

1599
    private buildPipeline = new ActionPipeline();
1,535✔
1600

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

1610
            const event = await this.plugins.emitAsync('beforeBuildProgram', {
309✔
1611
                program: this,
1612
                editor: this.editor,
1613
                files: options?.files ?? Object.values(this.files)
1,854✔
1614
            });
1615

1616
            //prepare the program (and files) for building
1617
            event.files = await this.prepare(event.files);
309✔
1618

1619
            //stage the entire program
1620
            const serializedFilesByFile = await this.serialize(event.files);
309✔
1621

1622
            await this.write(stagingDir, serializedFilesByFile);
309✔
1623

1624
            await this.plugins.emitAsync('afterBuildProgram', event);
309✔
1625

1626
            //undo all edits for the program
1627
            this.editor.undoAll();
309✔
1628
            //undo all edits for each file
1629
            for (const file of event.files) {
309✔
1630
                file.editor.undoAll();
626✔
1631
            }
1632
        });
1633
    }
1634

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

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

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

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

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

1698
        return files;
7✔
1699
    }
1700

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

1716
    private _manifest: Map<string, string>;
1717

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

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

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

1742
        // Set the updated bs_const value
1743
        parsedManifest.set('bs_const', constString);
13✔
1744
    }
1745

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

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

1771
    /**
1772
     * Get a map of the manifest information
1773
     */
1774
    public getManifest() {
1775
        if (!this._manifest) {
2,120✔
1776
            this.loadManifest();
1,355✔
1777
        }
1778
        return this._manifest;
2,120✔
1779
    }
1780

1781
    public dispose() {
1782
        this.plugins.emit('beforeProgramDispose', { program: this });
1,409✔
1783

1784
        for (let filePath in this.files) {
1,409✔
1785
            this.files[filePath]?.dispose?.();
1,910!
1786
        }
1787
        for (let name in this.scopes) {
1,409✔
1788
            this.scopes[name]?.dispose?.();
2,988!
1789
        }
1790
        this.globalScope?.dispose?.();
1,409!
1791
        this.dependencyGraph?.dispose?.();
1,409!
1792
    }
1793
}
1794

1795
export interface FileTranspileResult {
1796
    srcPath: string;
1797
    destPath: string;
1798
    pkgPath: string;
1799
    code: string;
1800
    map: string;
1801
    typedef: string;
1802
}
1803

1804

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

1816
    public srcExtension: string;
1817

1818
    public files: TFile[] = [];
2,203✔
1819
}
1820

1821
export interface ProgramBuildOptions {
1822
    /**
1823
     * The directory where the final built files should be placed. This directory will be cleared before running
1824
     */
1825
    stagingDir?: string;
1826
    /**
1827
     * An array of files to build. If omitted, the entire list of files from the program will be used instead.
1828
     * Typically you will want to leave this blank
1829
     */
1830
    files?: BscFile[];
1831
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc