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

rokucommunity / brighterscript / #15048

01 Jan 2026 11:17PM UTC coverage: 87.048% (-0.9%) from 87.907%
#15048

push

web-flow
Merge 02ba2bb57 into 2ea4d2108

14498 of 17595 branches covered (82.4%)

Branch coverage included in aggregate %.

192 of 261 new or added lines in 12 files covered. (73.56%)

897 existing lines in 48 files now uncovered.

15248 of 16577 relevant lines covered (91.98%)

24112.76 hits per line

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

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

65
const bslibNonAliasedRokuModulesPkgPath = s`source/roku_modules/rokucommunity_bslib/bslib.brs`;
1✔
66
const bslibAliasedRokuModulesPkgPath = s`source/roku_modules/bslib/bslib.brs`;
1✔
67

68
export interface SignatureInfoObj {
69
    index: number;
70
    key: string;
71
    signature: SignatureInformation;
72
}
73

74
export class Program {
1✔
75
    constructor(
76
        /**
77
         * The root directory for this program
78
         */
79
        options: BsConfig,
80
        logger?: Logger,
81
        plugins?: PluginInterface,
82
        diagnosticsManager?: DiagnosticManager
83
    ) {
84
        this.options = util.normalizeConfig(options);
2,123✔
85
        this.logger = logger ?? createLogger(options);
2,123✔
86
        this.plugins = plugins || new PluginInterface([], { logger: this.logger });
2,123✔
87
        this.diagnostics = diagnosticsManager || new DiagnosticManager();
2,123✔
88

89
        //try to find a location for the diagnostic if it doesn't have one
90
        this.diagnostics.locationResolver = (args) => {
2,123✔
91

92
            //find the first xml scope for this diagnostic
93
            for (let context of args.contexts) {
5✔
94
                if (isXmlScope(context.scope) && isXmlFile(context.scope.xmlFile)) {
1!
95
                    return util.createLocation(0, 0, 0, 100, context.scope.xmlFile.srcPath);
1✔
96
                }
97
            }
98

99
            //we couldn't find an xml scope for this, so try to find the manifest file instead
100
            const manifest = this.getFile('manifest', false);
4✔
101
            if (manifest) {
4✔
102
                return util.createLocation(0, 0, 0, 100, manifest.srcPath);
3✔
103
            }
104

105
            //if we still don't have a manifest, try to find the first file in the program
106
            for (const key in this.files) {
1✔
107
                if (isBrsFile(this.files[key]) || isXmlFile(this.files[key])) {
1!
108
                    return util.createLocation(0, 0, 0, 100, this.files[key].srcPath);
1✔
109
                }
110
            }
111

UNCOV
112
            this.logger.warn(`Unable to find a location for the diagnostic.`, args);
×
113

114
            //we couldn't find any locations for the file, so just return undefined
UNCOV
115
            return undefined;
×
116
        };
117

118
        // initialize the diagnostics Manager
119
        this.diagnostics.logger = this.logger;
2,123✔
120
        this.diagnostics.options = this.options;
2,123✔
121
        this.diagnostics.program = this;
2,123✔
122

123
        //inject the bsc plugin as the first plugin in the stack.
124
        this.plugins.addFirst(new BscPlugin());
2,123✔
125

126
        //normalize the root dir path
127
        this.options.rootDir = util.getRootDir(this.options);
2,123✔
128

129
        this.createGlobalScope();
2,123✔
130

131
        this.fileFactory = new FileFactory(this);
2,123✔
132
    }
133

134
    public options: FinalizedBsConfig;
135
    public logger: Logger;
136

137
    /**
138
     * 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`)
139
     */
140
    public editor = new Editor();
2,123✔
141

142
    /**
143
     * A factory that creates `File` instances
144
     */
145
    private fileFactory: FileFactory;
146

147
    private createGlobalScope() {
148
        //create the 'global' scope
149
        this.globalScope = new Scope('global', this, 'scope:global');
2,123✔
150
        this.globalScope.attachDependencyGraph(this.dependencyGraph);
2,123✔
151
        this.scopes.global = this.globalScope;
2,123✔
152

153
        this.populateGlobalSymbolTable();
2,123✔
154
        this.globalScope.symbolTable.addSibling(this.componentsTable);
2,123✔
155

156
        //hardcode the files list for global scope to only contain the global file
157
        this.globalScope.getAllFiles = () => [globalFile];
21,792✔
158
        globalFile.isValidated = true;
2,123✔
159
        this.globalScope.validate();
2,123✔
160

161
        //TODO we might need to fix this because the isValidated clears stuff now
162
        (this.globalScope as any).isValidated = true;
2,123✔
163
    }
164

165

166
    private recursivelyAddNodeToSymbolTable(nodeData: SGNodeData) {
167
        if (!nodeData) {
397,001!
UNCOV
168
            return;
×
169
        }
170
        let nodeType: ComponentType;
171
        const nodeName = util.getSgNodeTypeName(nodeData.name);
397,001✔
172
        if (!this.globalScope.symbolTable.hasSymbol(nodeName, SymbolTypeFlag.typetime)) {
397,001✔
173
            let parentNode: ComponentType;
174
            if (nodeData.extends) {
205,931✔
175
                const parentNodeData = nodes[nodeData.extends.name.toLowerCase()];
191,070✔
176
                try {
191,070✔
177
                    parentNode = this.recursivelyAddNodeToSymbolTable(parentNodeData);
191,070✔
178
                } catch (error) {
UNCOV
179
                    this.logger.error(error, nodeData);
×
180
                }
181
            }
182
            nodeType = new ComponentType(nodeData.name, parentNode);
205,931✔
183
            nodeType.addBuiltInInterfaces();
205,931✔
184
            nodeType.isBuiltIn = true;
205,931✔
185
            if (nodeData.name === 'Node') {
205,931✔
186
                // Add `roSGNode` as shorthand for `roSGNodeNode`
187
                this.globalScope.symbolTable.addSymbol('roSGNode', { description: nodeData.description, isBuiltIn: true }, nodeType, SymbolTypeFlag.typetime);
2,123✔
188
            }
189
            this.globalScope.symbolTable.addSymbol(nodeName, { description: nodeData.description, isBuiltIn: true }, nodeType, SymbolTypeFlag.typetime);
205,931✔
190
        } else {
191
            nodeType = this.globalScope.symbolTable.getSymbolType(nodeName, { flags: SymbolTypeFlag.typetime }) as ComponentType;
191,070✔
192
        }
193

194
        return nodeType;
397,001✔
195
    }
196
    /**
197
     * Do all setup required for the global symbol table.
198
     */
199
    private populateGlobalSymbolTable() {
200
        //Setup primitive types in global symbolTable
201

202
        const builtInSymbolData: ExtraSymbolData = { isBuiltIn: true };
2,123✔
203

204
        this.globalScope.symbolTable.addSymbol('boolean', builtInSymbolData, BooleanType.instance, SymbolTypeFlag.typetime);
2,123✔
205
        this.globalScope.symbolTable.addSymbol('double', builtInSymbolData, DoubleType.instance, SymbolTypeFlag.typetime);
2,123✔
206
        this.globalScope.symbolTable.addSymbol('dynamic', builtInSymbolData, DynamicType.instance, SymbolTypeFlag.typetime);
2,123✔
207
        this.globalScope.symbolTable.addSymbol('float', builtInSymbolData, FloatType.instance, SymbolTypeFlag.typetime);
2,123✔
208
        this.globalScope.symbolTable.addSymbol('function', builtInSymbolData, FunctionType.instance, SymbolTypeFlag.typetime);
2,123✔
209
        this.globalScope.symbolTable.addSymbol('integer', builtInSymbolData, IntegerType.instance, SymbolTypeFlag.typetime);
2,123✔
210
        this.globalScope.symbolTable.addSymbol('longinteger', builtInSymbolData, LongIntegerType.instance, SymbolTypeFlag.typetime);
2,123✔
211
        this.globalScope.symbolTable.addSymbol('object', builtInSymbolData, ObjectType.instance, SymbolTypeFlag.typetime);
2,123✔
212
        this.globalScope.symbolTable.addSymbol('string', builtInSymbolData, StringType.instance, SymbolTypeFlag.typetime);
2,123✔
213
        this.globalScope.symbolTable.addSymbol('void', builtInSymbolData, VoidType.instance, SymbolTypeFlag.typetime);
2,123✔
214

215
        BuiltInInterfaceAdder.getLookupTable = () => this.globalScope.symbolTable;
597,361✔
216

217
        for (const callable of globalCallables) {
2,123✔
218
            this.globalScope.symbolTable.addSymbol(callable.name, { ...builtInSymbolData, description: callable.shortDescription }, callable.type, SymbolTypeFlag.runtime);
163,471✔
219
        }
220

221
        for (const ifaceData of Object.values(interfaces) as BRSInterfaceData[]) {
2,123✔
222
            const ifaceType = new InterfaceType(ifaceData.name);
186,824✔
223
            ifaceType.addBuiltInInterfaces();
186,824✔
224
            ifaceType.isBuiltIn = true;
186,824✔
225
            this.globalScope.symbolTable.addSymbol(ifaceData.name, { ...builtInSymbolData, description: ifaceData.description }, ifaceType, SymbolTypeFlag.typetime);
186,824✔
226
        }
227

228
        for (const componentData of Object.values(components) as BRSComponentData[]) {
2,123✔
229
            let roComponentType: BscType;
230
            const lowerComponentName = componentData.name.toLowerCase();
142,241✔
231

232
            if (lowerComponentName === 'rosgnode') {
142,241✔
233
                // we will add `roSGNode` as shorthand for `roSGNodeNode`, since all roSgNode components are SceneGraph nodes
234
                continue;
2,123✔
235
            }
236
            if (lowerComponentName === 'rofunction') {
140,118✔
237
                roComponentType = new roFunctionType();
2,123✔
238
            } else {
239
                roComponentType = new InterfaceType(componentData.name);
137,995✔
240
            }
241
            roComponentType.addBuiltInInterfaces();
140,118✔
242
            roComponentType.isBuiltIn = true;
140,118✔
243
            this.globalScope.symbolTable.addSymbol(componentData.name, { ...builtInSymbolData, description: componentData.description }, roComponentType, SymbolTypeFlag.typetime);
140,118✔
244
        }
245

246
        for (const nodeData of Object.values(nodes) as SGNodeData[]) {
2,123✔
247
            this.recursivelyAddNodeToSymbolTable(nodeData);
205,931✔
248
        }
249

250
        for (const eventData of Object.values(events) as BRSEventData[]) {
2,123✔
251
            const eventType = new InterfaceType(eventData.name);
38,214✔
252
            eventType.addBuiltInInterfaces();
38,214✔
253
            eventType.isBuiltIn = true;
38,214✔
254
            this.globalScope.symbolTable.addSymbol(eventData.name, { ...builtInSymbolData, description: eventData.description }, eventType, SymbolTypeFlag.typetime);
38,214✔
255
        }
256

257
    }
258

259
    /**
260
     * A graph of all files and their dependencies.
261
     * For example:
262
     *      File.xml -> [lib1.brs, lib2.brs]
263
     *      lib2.brs -> [lib3.brs] //via an import statement
264
     */
265
    private dependencyGraph = new DependencyGraph();
2,123✔
266

267
    public diagnostics: DiagnosticManager;
268

269
    /**
270
     * A scope that contains all built-in global functions.
271
     * All scopes should directly or indirectly inherit from this scope
272
     */
273
    public globalScope: Scope = undefined as any;
2,123✔
274

275
    /**
276
     * Plugins which can provide extra diagnostics or transform AST
277
     */
278
    public plugins: PluginInterface;
279

280
    private fileSymbolInformation = new Map<string, { provides: ProvidedSymbolInfo; requires: UnresolvedSymbol[] }>();
2,123✔
281

282
    private currentScopeValidationOptions: ScopeValidationOptions;
283

284
    /**
285
     *  Map of typetime symbols which depend upon the key symbol
286
     */
287
    private symbolDependencies = new Map<string, Set<string>>();
2,123✔
288

289

290
    /**
291
     * Symbol Table for storing custom component types
292
     * This is a sibling to the global table (as Components can be used/referenced anywhere)
293
     * Keeping custom components out of the global table and in a specific symbol table
294
     * compartmentalizes their use
295
     */
296
    private componentsTable = new SymbolTable('Custom Components');
2,123✔
297

298
    public addFileSymbolInfo(file: BrsFile) {
299
        this.fileSymbolInformation.set(file.pkgPath, {
2,040✔
300
            provides: file.providedSymbols,
301
            requires: file.requiredSymbols
302
        });
303
    }
304

305
    public getFileSymbolInfo(file: BrsFile) {
306
        return this.fileSymbolInformation.get(file.pkgPath);
2,069✔
307
    }
308

309
    /**
310
     * The path to bslib.brs (the BrightScript runtime for certain BrighterScript features)
311
     */
312
    public get bslibPkgPath() {
313
        //if there's an aliased (preferred) version of bslib from roku_modules loaded into the program, use that
314
        if (this.getFile(bslibAliasedRokuModulesPkgPath)) {
2,686✔
315
            return bslibAliasedRokuModulesPkgPath;
11✔
316

317
            //if there's a non-aliased version of bslib from roku_modules, use that
318
        } else if (this.getFile(bslibNonAliasedRokuModulesPkgPath)) {
2,675✔
319
            return bslibNonAliasedRokuModulesPkgPath;
24✔
320

321
            //default to the embedded version
322
        } else {
323
            return `${this.options.bslibDestinationDir}${path.sep}bslib.brs`;
2,651✔
324
        }
325
    }
326

327
    public get bslibPrefix() {
328
        if (this.bslibPkgPath === bslibNonAliasedRokuModulesPkgPath) {
1,954✔
329
            return 'rokucommunity_bslib';
18✔
330
        } else {
331
            return 'bslib';
1,936✔
332
        }
333
    }
334

335

336
    /**
337
     * A map of every file loaded into this program, indexed by its original file location
338
     */
339
    public files = {} as Record<string, BscFile>;
2,123✔
340
    /**
341
     * A map of every file loaded into this program, indexed by its destPath
342
     */
343
    private destMap = new Map<string, BscFile>();
2,123✔
344
    /**
345
     * Plugins can contribute multiple virtual files for a single physical file.
346
     * This collection links the virtual files back to the physical file that produced them.
347
     * The key is the standardized and lower-cased srcPath
348
     */
349
    private fileClusters = new Map<string, BscFile[]>();
2,123✔
350

351
    private scopes = {} as Record<string, Scope>;
2,123✔
352

353
    protected addScope(scope: Scope) {
354
        this.scopes[scope.name] = scope;
2,291✔
355
        delete this.sortedScopeNames;
2,291✔
356
    }
357

358
    protected removeScope(scope: Scope) {
359
        if (this.scopes[scope.name]) {
16!
360
            delete this.scopes[scope.name];
16✔
361
            delete this.sortedScopeNames;
16✔
362
        }
363
    }
364

365
    /**
366
     * A map of every component currently loaded into the program, indexed by the component name.
367
     * It is a compile-time error to have multiple components with the same name. However, we store an array of components
368
     * by name so we can provide a better developer expreience. You shouldn't be directly accessing this array,
369
     * but if you do, only ever use the component at index 0.
370
     */
371
    private components = {} as Record<string, { file: XmlFile; scope: XmlScope }[]>;
2,123✔
372

373
    /**
374
     * Get the component with the specified name
375
     */
376
    public getComponent(componentName: string) {
377
        if (componentName) {
2,988✔
378
            //return the first compoment in the list with this name
379
            //(components are ordered in this list by destPath to ensure consistency)
380
            return this.components[componentName.toLowerCase()]?.[0];
2,954✔
381
        } else {
382
            return undefined;
34✔
383
        }
384
    }
385

386
    /**
387
     * Get the sorted names of custom components
388
     */
389
    public getSortedComponentNames() {
390
        const componentNames = Object.keys(this.components);
1,683✔
391
        componentNames.sort((a, b) => {
1,683✔
392
            if (a < b) {
846✔
393
                return -1;
291✔
394
            } else if (b < a) {
555!
395
                return 1;
555✔
396
            }
UNCOV
397
            return 0;
×
398
        });
399
        return componentNames;
1,683✔
400
    }
401

402
    /**
403
     * Keeps a set of all the components that need to have their types updated during the current validation cycle
404
     * Map <componentKey, componentName>
405
     */
406
    private componentSymbolsToUpdate = new Map<string, string>();
2,123✔
407

408
    /**
409
     * Register (or replace) the reference to a component in the component map
410
     */
411
    private registerComponent(xmlFile: XmlFile, scope: XmlScope) {
412
        const key = this.getComponentKey(xmlFile);
465✔
413
        if (!this.components[key]) {
465✔
414
            this.components[key] = [];
448✔
415
        }
416
        this.components[key].push({
465✔
417
            file: xmlFile,
418
            scope: scope
419
        });
420
        this.components[key].sort((a, b) => {
465✔
421
            const pathA = a.file.destPath.toLowerCase();
5✔
422
            const pathB = b.file.destPath.toLowerCase();
5✔
423
            if (pathA < pathB) {
5✔
424
                return -1;
1✔
425
            } else if (pathA > pathB) {
4!
426
                return 1;
4✔
427
            }
UNCOV
428
            return 0;
×
429
        });
430
        this.syncComponentDependencyGraph(this.components[key]);
465✔
431
        this.addDeferredComponentTypeSymbolCreation(xmlFile);
465✔
432
    }
433

434
    /**
435
     * Remove the specified component from the components map
436
     */
437
    private unregisterComponent(xmlFile: XmlFile) {
438
        const key = this.getComponentKey(xmlFile);
16✔
439
        const arr = this.components[key] || [];
16!
440
        for (let i = 0; i < arr.length; i++) {
16✔
441
            if (arr[i].file === xmlFile) {
16!
442
                arr.splice(i, 1);
16✔
443
                break;
16✔
444
            }
445
        }
446

447
        this.syncComponentDependencyGraph(arr);
16✔
448
        this.addDeferredComponentTypeSymbolCreation(xmlFile);
16✔
449
    }
450

451
    /**
452
     * Adds a component described in an XML to the set of components that needs to be updated this validation cycle.
453
     * @param xmlFile XML file with <component> tag
454
     */
455
    private addDeferredComponentTypeSymbolCreation(xmlFile: XmlFile) {
456
        const componentKey = this.getComponentKey(xmlFile);
956✔
457
        const componentName = xmlFile.componentName?.text;
956✔
458
        if (this.componentSymbolsToUpdate.has(componentKey)) {
956✔
459
            return;
495✔
460
        }
461
        this.componentSymbolsToUpdate.set(componentKey, componentName);
461✔
462
    }
463

464
    private getComponentKey(xmlFile: XmlFile) {
465
        return (xmlFile.componentName?.text ?? xmlFile.pkgPath).toLowerCase();
1,437✔
466
    }
467

468
    /**
469
     * Resolves symbol table with the first component in this.components to have the same name as the component in the file
470
     * @param componentKey key getting a component from `this.components`
471
     * @param componentName the unprefixed name of the component that will be added (e.g. 'MyLabel' NOT 'roSgNodeMyLabel')
472
     */
473
    private updateComponentSymbolInGlobalScope(componentKey: string, componentName: string) {
474
        const symbolName = componentName ? util.getSgNodeTypeName(componentName) : undefined;
412✔
475
        if (!symbolName) {
412✔
476
            return;
7✔
477
        }
478
        const components = this.components[componentKey] || [];
405!
479
        const previousComponentType = this.componentsTable.getSymbolType(symbolName, { flags: SymbolTypeFlag.typetime });
405✔
480
        // Remove any existing symbols that match
481
        this.componentsTable.removeSymbol(symbolName);
405✔
482
        if (components.length > 0) {
405✔
483
            // There is a component that can be added - use it.
484
            const componentScope = components[0].scope;
404✔
485

486
            this.componentsTable.removeSymbol(symbolName);
404✔
487
            componentScope.linkSymbolTable();
404✔
488
            const componentType = componentScope.getComponentType();
404✔
489
            if (componentType) {
404!
490
                this.componentsTable.addSymbol(symbolName, {}, componentType, SymbolTypeFlag.typetime);
404✔
491
            }
492
            const typeData = {};
404✔
493
            const isSameAsPrevious = previousComponentType && componentType.isEqual(previousComponentType, typeData);
404✔
494
            const isComponentTypeDifferent = !previousComponentType || isReferenceType(previousComponentType) || !isSameAsPrevious;
404✔
495
            componentScope.unlinkSymbolTable();
404✔
496
            return isComponentTypeDifferent;
404✔
497

498
        }
499
        // There was a previous component type, but no new one, so it's different
500
        return !!previousComponentType;
1✔
501
    }
502

503
    /**
504
     * Adds a reference type to the global symbol table with the first component in this.components to have the same name as the component in the file
505
     * This is so on a first validation, these types can be resolved in teh future (eg. when the actual component is created)
506
     * If we don't add reference types at this top level, they will be created at the file level, and will never get resolved
507
     * @param componentKey key getting a component from `this.components`
508
     * @param componentName the unprefixed name of the component that will be added (e.g. 'MyLabel' NOT 'roSgNodeMyLabel')
509
     */
510
    private addComponentReferenceType(componentKey: string, componentName: string) {
511
        const symbolName = componentName ? util.getSgNodeTypeName(componentName) : undefined;
408✔
512
        if (!symbolName) {
408✔
513
            return;
7✔
514
        }
515
        const components = this.components[componentKey] || [];
401!
516

517
        if (components.length > 0) {
401✔
518
            // There is a component that can be added,
519
            if (!this.componentsTable.hasSymbol(symbolName, SymbolTypeFlag.typetime)) {
400✔
520
                // it doesn't already exist in the table
521
                const componentRefType = new ReferenceType(symbolName, symbolName, SymbolTypeFlag.typetime, () => this.componentsTable);
4,692✔
522
                if (componentRefType) {
393!
523
                    this.componentsTable.addSymbol(symbolName, {}, componentRefType, SymbolTypeFlag.typetime);
393✔
524
                }
525
            }
526
        } else {
527
            // there is no component. remove from table
528
            this.componentsTable.removeSymbol(symbolName);
1✔
529
        }
530
    }
531

532
    /**
533
     * re-attach the dependency graph with a new key for any component who changed
534
     * their position in their own named array (only matters when there are multiple
535
     * components with the same name)
536
     */
537
    private syncComponentDependencyGraph(components: Array<{ file: XmlFile; scope: XmlScope }>) {
538
        //reattach every dependency graph
539
        for (let i = 0; i < components.length; i++) {
481✔
540
            const { file, scope } = components[i];
471✔
541

542
            //attach (or re-attach) the dependencyGraph for every component whose position changed
543
            if (file.dependencyGraphIndex !== i) {
471✔
544
                file.dependencyGraphIndex = i;
467✔
545
                this.dependencyGraph.addOrReplace(file.dependencyGraphKey, file.dependencies);
467✔
546
                file.attachDependencyGraph(this.dependencyGraph);
467✔
547
                scope.attachDependencyGraph(this.dependencyGraph);
467✔
548
            }
549
        }
550
    }
551

552
    /**
553
     * Get a list of all files that are included in the project but are not referenced
554
     * by any scope in the program.
555
     */
556
    public getUnreferencedFiles() {
UNCOV
557
        let result = [] as BscFile[];
×
UNCOV
558
        for (let filePath in this.files) {
×
UNCOV
559
            let file = this.files[filePath];
×
560
            //is this file part of a scope
UNCOV
561
            if (!this.getFirstScopeForFile(file)) {
×
562
                //no scopes reference this file. add it to the list
UNCOV
563
                result.push(file);
×
564
            }
565
        }
UNCOV
566
        return result;
×
567
    }
568

569
    /**
570
     * Get the list of errors for the entire program.
571
     */
572
    public getDiagnostics() {
573
        return this.diagnostics.getDiagnostics();
1,372✔
574
    }
575

576
    /**
577
     * Determine if the specified file is loaded in this program right now.
578
     * @param filePath the absolute or relative path to the file
579
     * @param normalizePath should the provided path be normalized before use
580
     */
581
    public hasFile(filePath: string, normalizePath = true) {
3,109✔
582
        return !!this.getFile(filePath, normalizePath);
3,109✔
583
    }
584

585
    /**
586
     * roku filesystem is case INsensitive, so find the scope by key case insensitive
587
     * @param scopeName xml scope names are their `destPath`. Source scope is stored with the key `"source"`
588
     */
589
    public getScopeByName(scopeName: string): Scope | undefined {
590
        if (!scopeName) {
72!
UNCOV
591
            return undefined;
×
592
        }
593
        //most scopes are xml file pkg paths. however, the ones that are not are single names like "global" and "scope",
594
        //so it's safe to run the standardizePkgPath method
595
        scopeName = s`${scopeName}`;
72✔
596
        let key = Object.keys(this.scopes).find(x => x.toLowerCase() === scopeName.toLowerCase());
161✔
597
        return this.scopes[key!];
72✔
598
    }
599

600
    /**
601
     * Return all scopes
602
     */
603
    public getScopes() {
604
        return Object.values(this.scopes);
13✔
605
    }
606

607
    /**
608
     * Find the scope for the specified component
609
     */
610
    public getComponentScope(componentName: string) {
611
        return this.getComponent(componentName)?.scope;
829✔
612
    }
613

614
    /**
615
     * Update internal maps with this file reference
616
     */
617
    private assignFile<T extends BscFile = BscFile>(file: T) {
618
        const fileAddEvent: BeforeFileAddEvent = {
2,842✔
619
            file: file,
620
            program: this
621
        };
622

623
        this.plugins.emit('beforeFileAdd', fileAddEvent);
2,842✔
624

625
        this.files[file.srcPath.toLowerCase()] = file;
2,842✔
626
        this.destMap.set(file.destPath.toLowerCase(), file);
2,842✔
627

628
        this.plugins.emit('afterFileAdd', fileAddEvent);
2,842✔
629

630
        return file;
2,842✔
631
    }
632

633
    /**
634
     * Remove this file from internal maps
635
     */
636
    private unassignFile<T extends BscFile = BscFile>(file: T) {
637
        delete this.files[file.srcPath.toLowerCase()];
222✔
638
        this.destMap.delete(file.destPath.toLowerCase());
222✔
639
        return file;
222✔
640
    }
641

642
    /**
643
     * Load a file into the program. If that file already exists, it is replaced.
644
     * If file contents are provided, those are used, Otherwise, the file is loaded from the file system
645
     * @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:/`)
646
     * @param fileData the file contents. omit or pass `undefined` to prevent loading the data at this time
647
     */
648
    public setFile<T extends BscFile>(srcDestOrPkgPath: string, fileData?: FileData): T;
649
    /**
650
     * Load a file into the program. If that file already exists, it is replaced.
651
     * @param fileEntry an object that specifies src and dest for the file.
652
     * @param fileData the file contents. omit or pass `undefined` to prevent loading the data at this time
653
     */
654
    public setFile<T extends BscFile>(fileEntry: FileObj, fileData: FileData): T;
655
    public setFile<T extends BscFile>(fileParam: FileObj | string, fileData: FileData): T {
656
        //normalize the file paths
657
        const { srcPath, destPath } = this.getPaths(fileParam, this.options.rootDir);
2,838✔
658

659
        let file = this.logger.time(LogLevel.debug, ['Program.setFile()', chalk.green(srcPath)], () => {
2,838✔
660
            //if the file is already loaded, remove it
661
            if (this.hasFile(srcPath)) {
2,838✔
662
                this.removeFile(srcPath, true, true);
202✔
663
            }
664

665
            const data = new LazyFileData(fileData);
2,838✔
666

667
            const event = new ProvideFileEventInternal(this, srcPath, destPath, data, this.fileFactory);
2,838✔
668

669
            this.plugins.emit('beforeProvideFile', event);
2,838✔
670
            this.plugins.emit('provideFile', event);
2,838✔
671
            this.plugins.emit('afterProvideFile', event);
2,838✔
672

673
            //if no files were provided, create a AssetFile to represent it.
674
            if (event.files.length === 0) {
2,838✔
675
                event.files.push(
36✔
676
                    this.fileFactory.AssetFile({
677
                        srcPath: event.srcPath,
678
                        destPath: event.destPath,
679
                        pkgPath: event.destPath,
680
                        data: data
681
                    })
682
                );
683
            }
684

685
            //find the file instance for the srcPath that triggered this action.
686
            const primaryFile = event.files.find(x => x.srcPath === srcPath);
2,838✔
687

688
            if (!primaryFile) {
2,838!
UNCOV
689
                throw new Error(`No file provided for srcPath '${srcPath}'. Instead, received ${JSON.stringify(event.files.map(x => ({
×
690
                    type: x.type,
691
                    srcPath: x.srcPath,
692
                    destPath: x.destPath
693
                })))}`);
694
            }
695

696
            //link the virtual files to the primary file
697
            this.fileClusters.set(primaryFile.srcPath?.toLowerCase(), event.files);
2,838!
698

699
            for (const file of event.files) {
2,838✔
700
                file.srcPath = s(file.srcPath);
2,842✔
701
                if (file.destPath) {
2,842!
702
                    file.destPath = s`${util.replaceCaseInsensitive(file.destPath, this.options.rootDir, '')}`;
2,842✔
703
                }
704
                if (file.pkgPath) {
2,842✔
705
                    file.pkgPath = s`${util.replaceCaseInsensitive(file.pkgPath, this.options.rootDir, '')}`;
2,838✔
706
                } else {
707
                    file.pkgPath = file.destPath;
4✔
708
                }
709
                file.excludeFromOutput = file.excludeFromOutput === true;
2,842✔
710

711
                //set the dependencyGraph key for every file to its destPath
712
                file.dependencyGraphKey = file.destPath.toLowerCase();
2,842✔
713

714
                this.assignFile(file);
2,842✔
715

716
                //register a callback anytime this file's dependencies change
717
                if (typeof file.onDependenciesChanged === 'function') {
2,842✔
718
                    file.disposables ??= [];
2,798!
719
                    file.disposables.push(
2,798✔
720
                        this.dependencyGraph.onchange(file.dependencyGraphKey, file.onDependenciesChanged.bind(file))
721
                    );
722
                }
723

724
                //register this file (and its dependencies) with the dependency graph
725
                this.dependencyGraph.addOrReplace(file.dependencyGraphKey, file.dependencies ?? []);
2,842✔
726

727
                //if this is a `source` file, add it to the source scope's dependency list
728
                if (this.isSourceBrsFile(file)) {
2,842✔
729
                    this.createSourceScope();
1,919✔
730
                    this.dependencyGraph.addDependency('scope:source', file.dependencyGraphKey);
1,919✔
731
                }
732

733
                //if this is an xml file in the components folder, register it as a component
734
                if (this.isComponentsXmlFile(file)) {
2,842✔
735
                    //create a new scope for this xml file
736
                    let scope = new XmlScope(file, this);
465✔
737
                    this.addScope(scope);
465✔
738

739
                    //register this componet now that we have parsed it and know its component name
740
                    this.registerComponent(file, scope);
465✔
741

742
                    //notify plugins that the scope is created and the component is registered
743
                    this.plugins.emit('afterScopeCreate', {
465✔
744
                        program: this,
745
                        scope: scope
746
                    });
747
                }
748
            }
749

750
            return primaryFile;
2,838✔
751
        });
752
        return file as T;
2,838✔
753
    }
754

755
    /**
756
     * Given a srcPath, a destPath, or both, resolve whichever is missing, relative to rootDir.
757
     * @param fileParam an object representing file paths
758
     * @param rootDir must be a pre-normalized path
759
     */
760
    private getPaths(fileParam: string | FileObj | { srcPath?: string; pkgPath?: string }, rootDir: string) {
761
        let srcPath: string | undefined;
762
        let destPath: string | undefined;
763

764
        assert.ok(fileParam, 'fileParam is required');
3,063✔
765

766
        //lift the path vars from the incoming param
767
        if (typeof fileParam === 'string') {
3,063✔
768
            fileParam = this.removePkgPrefix(fileParam);
2,544✔
769
            srcPath = s`${path.resolve(rootDir, fileParam)}`;
2,544✔
770
            destPath = s`${util.replaceCaseInsensitive(srcPath, rootDir, '')}`;
2,544✔
771
        } else {
772
            let param: any = fileParam;
519✔
773

774
            if (param.src) {
519✔
775
                srcPath = s`${param.src}`;
518✔
776
            }
777
            if (param.srcPath) {
519!
UNCOV
778
                srcPath = s`${param.srcPath}`;
×
779
            }
780
            if (param.dest) {
519✔
781
                destPath = s`${this.removePkgPrefix(param.dest)}`;
518✔
782
            }
783
            if (param.pkgPath) {
519!
UNCOV
784
                destPath = s`${this.removePkgPrefix(param.pkgPath)}`;
×
785
            }
786
        }
787

788
        //if there's no srcPath, use the destPath to build an absolute srcPath
789
        if (!srcPath) {
3,063✔
790
            srcPath = s`${rootDir}/${destPath}`;
1✔
791
        }
792
        //coerce srcPath to an absolute path
793
        if (!path.isAbsolute(srcPath)) {
3,063✔
794
            srcPath = util.standardizePath(srcPath);
1✔
795
        }
796

797
        //if destPath isn't set, compute it from the other paths
798
        if (!destPath) {
3,063✔
799
            destPath = s`${util.replaceCaseInsensitive(srcPath, rootDir, '')}`;
1✔
800
        }
801

802
        assert.ok(srcPath, 'fileEntry.src is required');
3,063✔
803
        assert.ok(destPath, 'fileEntry.dest is required');
3,063✔
804

805
        return {
3,063✔
806
            srcPath: srcPath,
807
            //remove leading slash
808
            destPath: destPath.replace(/^[\/\\]+/, '')
809
        };
810
    }
811

812
    /**
813
     * Remove any leading `pkg:/` found in the path
814
     */
815
    private removePkgPrefix(path: string) {
816
        return path.replace(/^pkg:\//i, '');
3,062✔
817
    }
818

819
    /**
820
     * Is this file a .brs file found somewhere within the `pkg:/source/` folder?
821
     */
822
    private isSourceBrsFile(file: BscFile) {
823
        return !!/^(pkg:\/)?source[\/\\]/.exec(file.destPath);
3,064✔
824
    }
825

826
    /**
827
     * Is this file a .brs file found somewhere within the `pkg:/source/` folder?
828
     */
829
    private isComponentsXmlFile(file: BscFile): file is XmlFile {
830
        return isXmlFile(file) && !!/^(pkg:\/)?components[\/\\]/.exec(file.destPath);
2,842✔
831
    }
832

833
    /**
834
     * Ensure source scope is created.
835
     * Note: automatically called internally, and no-op if it exists already.
836
     */
837
    public createSourceScope() {
838
        if (!this.scopes.source) {
2,820✔
839
            const sourceScope = new Scope('source', this, 'scope:source');
1,826✔
840
            sourceScope.attachDependencyGraph(this.dependencyGraph);
1,826✔
841
            this.addScope(sourceScope);
1,826✔
842
            this.plugins.emit('afterScopeCreate', {
1,826✔
843
                program: this,
844
                scope: sourceScope
845
            });
846
        }
847
    }
848

849
    /**
850
     * Remove a set of files from the program
851
     * @param srcPaths can be an array of srcPath or destPath strings
852
     * @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
853
     */
854
    public removeFiles(srcPaths: string[], normalizePath = true) {
1✔
855
        for (let srcPath of srcPaths) {
1✔
856
            this.removeFile(srcPath, normalizePath);
1✔
857
        }
858
    }
859

860
    /**
861
     * Remove a file from the program
862
     * @param filePath can be a srcPath, a destPath, or a destPath with leading `pkg:/`
863
     * @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
864
     */
865
    public removeFile(filePath: string, normalizePath = true, keepSymbolInformation = false) {
32✔
866
        this.logger.debug('Program.removeFile()', filePath);
220✔
867
        const paths = this.getPaths(filePath, this.options.rootDir);
220✔
868

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

872
        for (const file of files) {
220✔
873
            //if a file has already been removed, nothing more needs to be done here
874
            if (!file || !this.hasFile(file.srcPath)) {
223✔
875
                continue;
1✔
876
            }
877
            this.diagnostics.clearForFile(file.srcPath);
222✔
878

879
            const event: BeforeFileRemoveEvent = { file: file, program: this };
222✔
880
            this.plugins.emit('beforeFileRemove', event);
222✔
881

882
            //if there is a scope named the same as this file's path, remove it (i.e. xml scopes)
883
            let scope = this.scopes[file.destPath];
222✔
884
            if (scope) {
222✔
885
                this.logger.debug('Removing associated scope', scope.name);
16✔
886
                const scopeDisposeEvent = {
16✔
887
                    program: this,
888
                    scope: scope
889
                };
890
                this.plugins.emit('beforeScopeDispose', scopeDisposeEvent);
16✔
891
                this.plugins.emit('onScopeDispose', scopeDisposeEvent);
16✔
892
                scope.dispose();
16✔
893
                //notify dependencies of this scope that it has been removed
894
                this.dependencyGraph.remove(scope.dependencyGraphKey!);
16✔
895
                this.removeScope(this.scopes[file.destPath]);
16✔
896
                this.plugins.emit('afterScopeDispose', scopeDisposeEvent);
16✔
897
            }
898
            //remove the file from the program
899
            this.unassignFile(file);
222✔
900

901
            this.dependencyGraph.remove(file.dependencyGraphKey);
222✔
902

903
            //if this is a pkg:/source file, notify the `source` scope that it has changed
904
            if (this.isSourceBrsFile(file)) {
222✔
905
                this.dependencyGraph.removeDependency('scope:source', file.dependencyGraphKey);
151✔
906
            }
907
            if (isBrsFile(file)) {
222✔
908
                this.logger.debug('Removing file symbol info', file.srcPath);
199✔
909

910
                if (!keepSymbolInformation) {
199✔
911
                    this.fileSymbolInformation.delete(file.pkgPath);
12✔
912
                }
913
                this.crossScopeValidation.clearResolutionsForFile(file);
199✔
914
            }
915

916
            this.diagnostics.clearForFile(file.srcPath);
222✔
917

918
            //if this is a component, remove it from our components map
919
            if (isXmlFile(file)) {
222✔
920
                this.logger.debug('Unregistering component', file.srcPath);
16✔
921

922
                this.unregisterComponent(file);
16✔
923
            }
924
            this.logger.debug('Disposing file', file.srcPath);
222✔
925

926
            //dispose any disposable things on the file
927
            for (const disposable of file?.disposables ?? []) {
222!
928
                disposable();
215✔
929
            }
930
            //dispose file
931
            file?.dispose?.();
222!
932

933
            this.plugins.emit('afterFileRemove', event);
222✔
934
        }
935
    }
936

937
    public crossScopeValidation = new CrossScopeValidator(this);
2,123✔
938

939
    private isFirstValidation = true;
2,123✔
940

941
    private validationDetails: {
2,123✔
942
        brsFilesValidated: BrsFile[];
943
        xmlFilesValidated: XmlFile[];
944
        changedSymbols: Map<SymbolTypeFlag, Set<string>>;
945
        changedComponentTypes: string[];
946
        scopesToValidate: Scope[];
947
        filesToBeValidatedInScopeContext: Set<BscFile>;
948

949
    } = {
950
            brsFilesValidated: [],
951
            xmlFilesValidated: [],
952
            changedSymbols: new Map<SymbolTypeFlag, Set<string>>(),
953
            changedComponentTypes: [],
954
            scopesToValidate: [],
955
            filesToBeValidatedInScopeContext: new Set<BscFile>()
956
        };
957

958
    public lastValidationInfo: {
2,123✔
959
        brsFilesSrcPath: Set<string>;
960
        xmlFilesSrcPath: Set<string>;
961
        scopeNames: Set<string>;
962
        componentsRebuilt: Set<string>;
963
    } = {
964
            brsFilesSrcPath: new Set<string>(),
965
            xmlFilesSrcPath: new Set<string>(),
966
            scopeNames: new Set<string>(),
967
            componentsRebuilt: new Set<string>()
968
        };
969

970
    /**
971
     * Counter used to track which validation run is being logged
972
     */
973
    private validationRunSequence = 1;
2,123✔
974

975
    /**
976
     * How many milliseconds can pass while doing synchronous operations in validate before we register a short timeout (i.e. yield to the event loop)
977
     */
978
    private validationMinSyncDuration = 75;
2,123✔
979

980
    private validatePromise: Promise<void> | undefined;
981

982
    /**
983
     * Traverse the entire project, and validate all scopes
984
     */
985
    public validate(): void;
986
    public validate(options: { async: false; cancellationToken?: CancellationToken }): void;
987
    public validate(options: { async: true; cancellationToken?: CancellationToken }): Promise<void>;
988
    public validate(options?: { async?: boolean; cancellationToken?: CancellationToken }) {
989
        const validationRunId = this.validationRunSequence++;
1,691✔
990

991
        let previousValidationPromise = this.validatePromise;
1,691✔
992
        const deferred = new Deferred();
1,691✔
993

994
        if (options?.async) {
1,691✔
995
            //we're async, so create a new promise chain to resolve after this validation is done
996
            this.validatePromise = Promise.resolve(previousValidationPromise).then(() => {
157✔
997
                return deferred.promise;
157✔
998
            });
999

1000
            //we are not async but there's a pending promise, then we cannot run this validation
1001
        } else if (previousValidationPromise !== undefined) {
1,534!
UNCOV
1002
            throw new Error('Cannot run synchronous validation while an async validation is in progress');
×
1003
        }
1004

1005
        let beforeProgramValidateWasEmitted = false;
1,691✔
1006

1007
        const brsFilesValidated: BrsFile[] = this.validationDetails.brsFilesValidated;
1,691✔
1008
        const xmlFilesValidated: XmlFile[] = this.validationDetails.xmlFilesValidated;
1,691✔
1009
        const changedSymbols = this.validationDetails.changedSymbols;
1,691✔
1010
        const changedComponentTypes = this.validationDetails.changedComponentTypes;
1,691✔
1011
        const scopesToValidate = this.validationDetails.scopesToValidate;
1,691✔
1012
        const filesToBeValidatedInScopeContext = this.validationDetails.filesToBeValidatedInScopeContext;
1,691✔
1013

1014
        //validate every file
1015

1016
        let logValidateEnd = (status?: string) => { };
1,691✔
1017

1018
        //will be populated later on during the correspnding sequencer event
1019
        let filesToProcess: BscFile[];
1020

1021
        const sequencer = new Sequencer({
1,691✔
1022
            name: 'program.validate',
1023
            cancellationToken: options?.cancellationToken ?? new CancellationTokenSource().token,
10,146✔
1024
            minSyncDuration: this.validationMinSyncDuration
1025
        });
1026
        //this sequencer allows us to run in both sync and async mode, depending on whether options.async is enabled.
1027
        //We use this to prevent starving the CPU during long validate cycles when running in a language server context
1028
        sequencer
1,691✔
1029
            .once('wait for previous run', () => {
1030
                //if running in async mode, return the previous validation promise to ensure we're only running one at a time
1031
                if (options?.async) {
1,691✔
1032
                    return previousValidationPromise;
157✔
1033
                }
1034
            })
1035
            .once('before and on programValidate', () => {
1036
                logValidateEnd = this.logger.timeStart(LogLevel.log, `Validating project${(this.logger.logLevel as LogLevel) > LogLevel.log ? ` (run ${validationRunId})` : ''}`);
1,687!
1037
                this.diagnostics.clearForTag(ProgramValidatorDiagnosticsTag);
1,687✔
1038
                this.plugins.emit('beforeProgramValidate', {
1,687✔
1039
                    program: this
1040
                });
1041
                beforeProgramValidateWasEmitted = true;
1,687✔
1042
                this.plugins.emit('onProgramValidate', {
1,687✔
1043
                    program: this
1044
                });
1045
            })
1046
            .once('get files to be validated', () => {
1047
                filesToProcess = Object.values(this.files).sort(firstBy(x => x.srcPath)).filter(x => !x.isValidated);
4,454✔
1048
                for (const file of filesToProcess) {
1,687✔
1049
                    filesToBeValidatedInScopeContext.add(file);
2,393✔
1050
                }
1051
            })
1052
            .once('add component reference types', () => {
1053
                // Create reference component types for any component that changes
1054
                for (let [componentKey, componentName] of this.componentSymbolsToUpdate.entries()) {
1,684✔
1055
                    this.addComponentReferenceType(componentKey, componentName);
408✔
1056
                }
1057
            })
1058
            .forEach('beforeFileValidate', () => filesToProcess, (file) => {
1,684✔
1059
                //run the beforeFilevalidate event for every unvalidated file
1060
                this.plugins.emit('beforeFileValidate', {
2,392✔
1061
                    program: this,
1062
                    file: file
1063
                });
1064
            })
1065
            .forEach('onFileValidate', () => filesToProcess, (file) => {
1,684✔
1066
                //run the onFileValidate event for every unvalidated file
1067
                this.plugins.emit('onFileValidate', {
2,392✔
1068
                    program: this,
1069
                    file: file
1070
                });
1071
                file.isValidated = true;
2,392✔
1072
                if (isBrsFile(file)) {
2,392✔
1073
                    brsFilesValidated.push(file);
1,982✔
1074
                } else if (isXmlFile(file)) {
410!
1075
                    xmlFilesValidated.push(file);
410✔
1076
                }
1077
            })
1078
            .forEach('afterFileValidate', () => filesToProcess, (file) => {
1,684✔
1079
                //run the onFileValidate event for every unvalidated file
1080
                this.plugins.emit('afterFileValidate', {
2,392✔
1081
                    program: this,
1082
                    file: file
1083
                });
1084
            })
1085
            .forEach('do deferred component creation', () => [...brsFilesValidated, ...xmlFilesValidated], (file) => {
1,683✔
1086
                if (isXmlFile(file)) {
2,392✔
1087
                    this.addDeferredComponentTypeSymbolCreation(file);
410✔
1088
                } else if (isBrsFile(file)) {
1,982!
1089
                    const fileHasChanges = file.providedSymbols.changes.get(SymbolTypeFlag.runtime).size > 0 || file.providedSymbols.changes.get(SymbolTypeFlag.typetime).size > 0;
1,982✔
1090
                    if (fileHasChanges) {
1,982✔
1091
                        for (const scope of this.getScopesForFile(file)) {
1,817✔
1092
                            if (isXmlScope(scope) && this.doesXmlFileRequireProvidedSymbols(scope.xmlFile, file.providedSymbols.changes)) {
2,077✔
1093
                                this.addDeferredComponentTypeSymbolCreation(scope.xmlFile);
65✔
1094
                            }
1095
                        }
1096
                    }
1097
                }
1098
            })
1099
            .once('build component types for any component that changes', () => {
1100
                this.logger.time(LogLevel.info, ['Build component types'], () => {
1,683✔
1101
                    this.logger.debug(`Component Symbols to update:`, [...this.componentSymbolsToUpdate.entries()].sort());
1,683✔
1102
                    this.lastValidationInfo.componentsRebuilt = new Set<string>();
1,683✔
1103
                    for (let [componentKey, componentName] of this.componentSymbolsToUpdate.entries()) {
1,683✔
1104
                        this.lastValidationInfo.componentsRebuilt.add(componentName?.toLowerCase());
412✔
1105
                        if (this.updateComponentSymbolInGlobalScope(componentKey, componentName)) {
412✔
1106
                            changedComponentTypes.push(util.getSgNodeTypeName(componentName).toLowerCase());
402✔
1107
                        }
1108
                    }
1109
                    this.componentSymbolsToUpdate.clear();
1,683✔
1110
                });
1111
            })
1112
            .once('track and update type-time and runtime symbol dependencies and changes', () => {
1113
                const changedSymbolsMapArr = [...brsFilesValidated, ...xmlFilesValidated]?.map(f => {
1,683!
1114
                    if (isBrsFile(f)) {
2,392✔
1115
                        return f.providedSymbols.changes;
1,982✔
1116
                    }
1117
                    return null;
410✔
1118
                }).filter(x => x);
2,392✔
1119

1120
                // update the map of typetime dependencies
1121
                for (const file of brsFilesValidated) {
1,683✔
1122
                    for (const [symbolName, provided] of file.providedSymbols.symbolMap.get(SymbolTypeFlag.typetime).entries()) {
1,982✔
1123
                        // clear existing dependencies
1124
                        for (const values of this.symbolDependencies.values()) {
739✔
1125
                            values.delete(symbolName);
62✔
1126
                        }
1127

1128
                        // map types to the set of types that depend upon them
1129
                        for (const dependentSymbol of provided.requiredSymbolNames?.values() ?? []) {
739!
1130
                            const dependentSymbolLower = dependentSymbol.toLowerCase();
195✔
1131
                            if (!this.symbolDependencies.has(dependentSymbolLower)) {
195✔
1132
                                this.symbolDependencies.set(dependentSymbolLower, new Set<string>());
173✔
1133
                            }
1134
                            const symbolsDependentUpon = this.symbolDependencies.get(dependentSymbolLower);
195✔
1135
                            symbolsDependentUpon.add(symbolName);
195✔
1136
                        }
1137
                    }
1138
                }
1139

1140
                for (const flag of [SymbolTypeFlag.runtime, SymbolTypeFlag.typetime]) {
1,683✔
1141
                    const changedSymbolsSetArr = changedSymbolsMapArr.map(symMap => symMap.get(flag));
3,964✔
1142
                    const changedSymbolSet = new Set<string>();
3,366✔
1143
                    for (const changeSet of changedSymbolsSetArr) {
3,366✔
1144
                        for (const change of changeSet) {
3,964✔
1145
                            changedSymbolSet.add(change);
3,878✔
1146
                        }
1147
                    }
1148
                    if (!changedSymbols.has(flag)) {
3,366✔
1149
                        changedSymbols.set(flag, changedSymbolSet);
3,362✔
1150
                    } else {
1151
                        changedSymbols.set(flag, new Set([...changedSymbols.get(flag), ...changedSymbolSet]));
4✔
1152
                    }
1153
                }
1154

1155
                // update changed symbol set with any changed component
1156
                for (const changedComponentType of changedComponentTypes) {
1,683✔
1157
                    changedSymbols.get(SymbolTypeFlag.typetime).add(changedComponentType);
402✔
1158
                }
1159

1160
                // Add any additional types that depend on a changed type
1161
                // as each iteration of the loop might add new types, need to keep checking until nothing new is added
1162
                const dependentTypesChanged = new Set<string>();
1,683✔
1163
                let foundDependentTypes = false;
1,683✔
1164
                const changedTypeSymbols = changedSymbols.get(SymbolTypeFlag.typetime);
1,683✔
1165
                do {
1,683✔
1166
                    foundDependentTypes = false;
1,689✔
1167
                    const allChangedTypesSofar = [...Array.from(changedTypeSymbols), ...Array.from(dependentTypesChanged)];
1,689✔
1168
                    for (const changedSymbol of allChangedTypesSofar) {
1,689✔
1169
                        const symbolsDependentUponChangedSymbol = this.symbolDependencies.get(changedSymbol) ?? [];
1,143✔
1170
                        for (const symbolName of symbolsDependentUponChangedSymbol) {
1,143✔
1171
                            if (!changedTypeSymbols.has(symbolName) && !dependentTypesChanged.has(symbolName)) {
194✔
1172
                                foundDependentTypes = true;
6✔
1173
                                dependentTypesChanged.add(symbolName);
6✔
1174
                            }
1175
                        }
1176
                    }
1177
                } while (foundDependentTypes);
1178

1179
                changedSymbols.set(SymbolTypeFlag.typetime, new Set([...changedSymbols.get(SymbolTypeFlag.typetime), ...changedTypeSymbols, ...dependentTypesChanged]));
1,683✔
1180

1181
                this.lastValidationInfo.brsFilesSrcPath = new Set<string>(this.validationDetails.brsFilesValidated.map(f => f.srcPath?.toLowerCase() ?? ''));
1,980!
1182
                this.lastValidationInfo.xmlFilesSrcPath = new Set<string>(this.validationDetails.xmlFilesValidated.map(f => f.srcPath?.toLowerCase() ?? ''));
1,683!
1183

1184
                // can reset filesValidatedList, because they are no longer needed
1185
                this.validationDetails.brsFilesValidated = [];
1,683✔
1186
                this.validationDetails.xmlFilesValidated = [];
1,683✔
1187
            })
1188
            .once('tracks changed symbols and prepares files and scopes for validation', () => {
1189
                if (this.options.logLevel === LogLevel.debug) {
1,683!
UNCOV
1190
                    const changedRuntime = Array.from(changedSymbols.get(SymbolTypeFlag.runtime)).sort();
×
UNCOV
1191
                    this.logger.debug('Changed Symbols (runTime):', changedRuntime.join(', '));
×
UNCOV
1192
                    const changedTypetime = Array.from(changedSymbols.get(SymbolTypeFlag.typetime)).sort();
×
UNCOV
1193
                    this.logger.debug('Changed Symbols (typeTime):', changedTypetime.join(', '));
×
1194
                }
1195
                const didComponentChange = changedComponentTypes.length > 0;
1,683✔
1196
                const didProvidedSymbolChange = changedSymbols.get(SymbolTypeFlag.runtime).size > 0 || changedSymbols.get(SymbolTypeFlag.typetime).size > 0;
1,683✔
1197
                const scopesToCheck = this.getScopesForCrossScopeValidation(didComponentChange, didProvidedSymbolChange);
1,683✔
1198

1199
                this.crossScopeValidation.buildComponentsMap();
1,683✔
1200
                this.logger.time(LogLevel.info, ['addDiagnosticsForScopes'], () => {
1,683✔
1201
                    this.crossScopeValidation.addDiagnosticsForScopes(scopesToCheck);
1,683✔
1202
                });
1203
                const filesToRevalidate = this.crossScopeValidation.getFilesRequiringChangedSymbol(scopesToCheck, changedSymbols);
1,683✔
1204
                for (const file of filesToRevalidate) {
1,683✔
1205
                    filesToBeValidatedInScopeContext.add(file);
445✔
1206
                }
1207

1208
                this.currentScopeValidationOptions = {
1,683✔
1209
                    filesToBeValidatedInScopeContext: filesToBeValidatedInScopeContext,
1210
                    changedSymbols: changedSymbols,
1211
                    changedFiles: Array.from(filesToBeValidatedInScopeContext),
1212
                    initialValidation: this.isFirstValidation
1213
                };
1214

1215
                //can reset changedComponent types
1216
                this.validationDetails.changedComponentTypes = [];
1,683✔
1217
            })
1218
            .forEach('invalidate affected scopes', () => filesToBeValidatedInScopeContext, (file) => {
1,683✔
1219
                if (isBrsFile(file)) {
2,524✔
1220
                    file.validationSegmenter.unValidateAllSegments();
2,114✔
1221
                    for (const scope of this.getScopesForFile(file)) {
2,114✔
1222
                        scope.invalidate();
2,396✔
1223
                    }
1224
                }
1225
            })
1226
            .once('checking scopes to validate', () => {
1227
                //sort the scope names so we get consistent results
1228
                for (const scopeName of this.getSortedScopeNames()) {
1,683✔
1229
                    let scope = this.scopes[scopeName];
3,795✔
1230
                    if (scope.shouldValidate(this.currentScopeValidationOptions)) {
3,795✔
1231
                        scopesToValidate.push(scope);
2,053✔
1232
                    }
1233
                }
1234
                this.lastValidationInfo.scopeNames = new Set<string>(scopesToValidate.map(s => s.name?.toLowerCase() ?? ''));
2,058!
1235
            })
1236
            .forEach('beforeScopeValidate', () => scopesToValidate, (scope) => {
1,682✔
1237
                this.plugins.emit('beforeScopeValidate', {
2,058✔
1238
                    program: this,
1239
                    scope: scope
1240
                });
1241
            })
1242
            .forEach('validate scope', () => scopesToValidate, (scope) => {
1,681✔
1243
                scope.validate(this.currentScopeValidationOptions);
2,056✔
1244
            })
1245
            .forEach('afterScopeValidate', () => scopesToValidate, (scope) => {
1,677✔
1246
                this.plugins.emit('afterScopeValidate', {
2,049✔
1247
                    program: this,
1248
                    scope: scope
1249
                });
1250
            })
1251
            .once('detect duplicate component names', () => {
1252
                this.detectDuplicateComponentNames();
1,676✔
1253
                this.isFirstValidation = false;
1,676✔
1254

1255
                // can reset other validation details
1256
                this.validationDetails.changedSymbols = new Map<SymbolTypeFlag, Set<string>>();
1,676✔
1257
                this.validationDetails.scopesToValidate = [];
1,676✔
1258
                this.validationDetails.filesToBeValidatedInScopeContext = new Set<BscFile>();
1,676✔
1259

1260
            })
1261
            .onCancel(() => {
1262
                logValidateEnd('cancelled');
15✔
1263
            })
1264
            .onSuccess(() => {
1265
                logValidateEnd();
1,676✔
1266
            })
1267
            .onComplete(() => {
1268
                //if we emitted the beforeProgramValidate hook, emit the afterProgramValidate hook as well
1269
                if (beforeProgramValidateWasEmitted) {
1,691✔
1270
                    const wasCancelled = options?.cancellationToken?.isCancellationRequested ?? false;
1,687✔
1271
                    this.plugins.emit('afterProgramValidate', {
1,687✔
1272
                        program: this,
1273
                        wasCancelled: wasCancelled
1274
                    });
1275
                }
1276

1277
                //log all the sequencer timing metrics if `info` logging is enabled
1278
                this.logger.info(
1,691✔
1279
                    sequencer.formatMetrics({
1280
                        header: 'Program.validate metrics:',
1281
                        //only include loop iterations if `debug` logging is enabled
1282
                        includeLoopIterations: this.logger.isLogLevelEnabled(LogLevel.debug)
1283
                    })
1284
                );
1285

1286
                //regardless of the success of the validation, mark this run as complete
1287
                deferred.resolve();
1,691✔
1288
                //clear the validatePromise which means we're no longer running a validation
1289
                this.validatePromise = undefined;
1,691✔
1290
            });
1291

1292
        //run the sequencer in async mode if enabled
1293
        if (options?.async) {
1,691✔
1294
            return sequencer.run();
157✔
1295

1296
            //run the sequencer in sync mode
1297
        } else {
1298
            return sequencer.runSync();
1,534✔
1299
        }
1300
    }
1301

1302
    private getScopesForCrossScopeValidation(someComponentTypeChanged: boolean, didProvidedSymbolChange: boolean) {
1303
        const scopesForCrossScopeValidation: Scope[] = [];
1,683✔
1304
        for (let scopeName of this.getSortedScopeNames()) {
1,683✔
1305
            let scope = this.scopes[scopeName];
3,795✔
1306
            if (this.globalScope === scope) {
3,795✔
1307
                continue;
1,683✔
1308
            }
1309
            if (someComponentTypeChanged) {
2,112✔
1310
                scopesForCrossScopeValidation.push(scope);
562✔
1311
            }
1312
            if (didProvidedSymbolChange && !scope.isValidated) {
2,112✔
1313
                scopesForCrossScopeValidation.push(scope);
1,929✔
1314
            }
1315
        }
1316
        return scopesForCrossScopeValidation;
1,683✔
1317
    }
1318

1319
    private doesXmlFileRequireProvidedSymbols(file: XmlFile, providedSymbolsByFlag: Map<SymbolTypeFlag, Set<string>>) {
1320
        for (const required of file.requiredSymbols) {
585✔
1321
            const symbolNameLower = (required as UnresolvedXMLSymbol).name.toLowerCase();
73✔
1322
            const requiredSymbolIsProvided = providedSymbolsByFlag.get(required.flags).has(symbolNameLower);
73✔
1323
            if (requiredSymbolIsProvided) {
73✔
1324
                return true;
65✔
1325
            }
1326
        }
1327
        return false;
520✔
1328
    }
1329

1330
    /**
1331
     * Flag all duplicate component names
1332
     */
1333
    private detectDuplicateComponentNames() {
1334
        const componentsByName = Object.keys(this.files).reduce<Record<string, XmlFile[]>>((map, filePath) => {
1,676✔
1335
            const file = this.files[filePath];
2,783✔
1336
            //if this is an XmlFile, and it has a valid `componentName` property
1337
            if (isXmlFile(file) && file.componentName?.text) {
2,783✔
1338
                let lowerName = file.componentName.text.toLowerCase();
576✔
1339
                if (!map[lowerName]) {
576✔
1340
                    map[lowerName] = [];
573✔
1341
                }
1342
                map[lowerName].push(file);
576✔
1343
            }
1344
            return map;
2,783✔
1345
        }, {});
1346

1347
        for (let name in componentsByName) {
1,676✔
1348
            const xmlFiles = componentsByName[name];
573✔
1349
            //add diagnostics for every duplicate component with this name
1350
            if (xmlFiles.length > 1) {
573✔
1351
                for (let xmlFile of xmlFiles) {
3✔
1352
                    const { componentName } = xmlFile;
6✔
1353
                    this.diagnostics.register({
6✔
1354
                        ...DiagnosticMessages.duplicateComponentName(componentName.text),
1355
                        location: xmlFile.componentName.location,
1356
                        relatedInformation: xmlFiles.filter(x => x !== xmlFile).map(x => {
12✔
1357
                            return {
6✔
1358
                                location: x.componentName.location,
1359
                                message: 'Also defined here'
1360
                            };
1361
                        })
1362
                    }, { tags: [ProgramValidatorDiagnosticsTag] });
1363
                }
1364
            }
1365
        }
1366
    }
1367

1368
    /**
1369
     * Get the files for a list of filePaths
1370
     * @param filePaths can be an array of srcPath or a destPath strings
1371
     * @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
1372
     */
1373
    public getFiles<T extends BscFile>(filePaths: string[], normalizePath = true) {
29✔
1374
        return filePaths
29✔
1375
            .map(filePath => this.getFile(filePath, normalizePath))
39✔
1376
            .filter(file => file !== undefined) as T[];
39✔
1377
    }
1378

1379
    private getFilePathCache = new Map<string, { path: string; isDestMap?: boolean }>();
2,123✔
1380

1381
    /**
1382
     * Get the file at the given path
1383
     * @param filePath can be a srcPath or a destPath
1384
     * @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
1385
     */
1386
    public getFile<T extends BscFile>(filePath: string, normalizePath = true) {
21,095✔
1387
        if (this.getFilePathCache.has(filePath)) {
28,253✔
1388
            const cachedFilePath = this.getFilePathCache.get(filePath);
16,486✔
1389
            if (cachedFilePath.isDestMap) {
16,486✔
1390
                return this.destMap.get(
13,625✔
1391
                    cachedFilePath.path
1392
                ) as T;
1393
            }
1394
            return this.files[
2,861✔
1395
                cachedFilePath.path
1396
            ] as T;
1397
        }
1398
        if (typeof filePath !== 'string') {
11,767✔
1399
            return undefined;
3,957✔
1400
            //is the path absolute (or the `virtual:` prefix)
1401
        } else if (/^(?:(?:virtual:[\/\\])|(?:\w:)|(?:[\/\\]))/gmi.exec(filePath)) {
7,810✔
1402
            const standardizedPath = (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase();
3,737!
1403
            this.getFilePathCache.set(filePath, { path: standardizedPath });
3,737✔
1404

1405
            return this.files[
3,737✔
1406
                standardizedPath
1407
            ] as T;
1408
        } else if (util.isUriLike(filePath)) {
4,073✔
1409
            const path = URI.parse(filePath).fsPath;
615✔
1410
            const standardizedPath = (normalizePath ? util.standardizePath(path) : path).toLowerCase();
615!
1411
            this.getFilePathCache.set(filePath, { path: standardizedPath });
615✔
1412

1413
            return this.files[
615✔
1414
                standardizedPath
1415
            ] as T;
1416
        } else {
1417
            const standardizedPath = (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase();
3,458✔
1418
            this.getFilePathCache.set(filePath, { path: standardizedPath, isDestMap: true });
3,458✔
1419
            return this.destMap.get(
3,458✔
1420
                standardizedPath
1421
            ) as T;
1422
        }
1423
    }
1424

1425
    private sortedScopeNames: string[] = undefined;
2,123✔
1426

1427
    /**
1428
     * Gets a sorted list of all scopeNames, always beginning with "global", "source", then any others in alphabetical order
1429
     */
1430
    private getSortedScopeNames() {
1431
        if (!this.sortedScopeNames) {
12,494✔
1432
            this.sortedScopeNames = Object.keys(this.scopes).sort((a, b) => {
1,609✔
1433
                if (a === 'global') {
2,253!
UNCOV
1434
                    return -1;
×
1435
                } else if (b === 'global') {
2,253✔
1436
                    return 1;
1,550✔
1437
                }
1438
                if (a === 'source') {
703✔
1439
                    return -1;
28✔
1440
                } else if (b === 'source') {
675✔
1441
                    return 1;
164✔
1442
                }
1443
                if (a < b) {
511✔
1444
                    return -1;
197✔
1445
                } else if (b < a) {
314!
1446
                    return 1;
314✔
1447
                }
UNCOV
1448
                return 0;
×
1449
            });
1450
        }
1451
        return this.sortedScopeNames;
12,494✔
1452
    }
1453

1454
    /**
1455
     * Get a list of all scopes the file is loaded into
1456
     * @param file the file
1457
     */
1458
    public getScopesForFile(file: BscFile | string) {
1459
        const resolvedFile = typeof file === 'string' ? this.getFile(file) : file;
4,522✔
1460

1461
        let result = [] as Scope[];
4,522✔
1462
        if (resolvedFile) {
4,522✔
1463
            const scopeKeys = this.getSortedScopeNames();
4,521✔
1464
            for (let key of scopeKeys) {
4,521✔
1465
                let scope = this.scopes[key];
40,508✔
1466

1467
                if (scope.hasFile(resolvedFile)) {
40,508✔
1468
                    result.push(scope);
5,079✔
1469
                }
1470
            }
1471
        }
1472
        return result;
4,522✔
1473
    }
1474

1475
    /**
1476
     * Get the first found scope for a file.
1477
     */
1478
    public getFirstScopeForFile(file: BscFile): Scope | undefined {
1479
        const scopeKeys = this.getSortedScopeNames();
4,607✔
1480
        for (let key of scopeKeys) {
4,607✔
1481
            let scope = this.scopes[key];
19,687✔
1482

1483
            if (scope.hasFile(file)) {
19,687✔
1484
                return scope;
3,371✔
1485
            }
1486
        }
1487
    }
1488

1489
    public getStatementsByName(name: string, originFile: BrsFile, namespaceName?: string): FileLink<Statement>[] {
1490
        let results = new Map<Statement, FileLink<Statement>>();
39✔
1491
        const filesSearched = new Set<BrsFile>();
39✔
1492
        let lowerNamespaceName = namespaceName?.toLowerCase();
39✔
1493
        let lowerName = name?.toLowerCase();
39!
1494

1495
        function addToResults(statement: FunctionStatement | MethodStatement, file: BrsFile) {
1496
            let parentNamespaceName = statement.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(originFile.parseMode)?.toLowerCase();
98✔
1497
            if (statement.tokens.name.text.toLowerCase() === lowerName && (!lowerNamespaceName || parentNamespaceName === lowerNamespaceName)) {
98✔
1498
                if (!results.has(statement)) {
36!
1499
                    results.set(statement, { item: statement, file: file as BrsFile });
36✔
1500
                }
1501
            }
1502
        }
1503

1504
        //look through all files in scope for matches
1505
        for (const scope of this.getScopesForFile(originFile)) {
39✔
1506
            for (const file of scope.getAllFiles()) {
39✔
1507
                //skip non-brs files, or files we've already processed
1508
                if (!isBrsFile(file) || filesSearched.has(file)) {
45✔
1509
                    continue;
3✔
1510
                }
1511
                filesSearched.add(file);
42✔
1512

1513
                file.ast.walk(createVisitor({
42✔
1514
                    FunctionStatement: (statement: FunctionStatement) => {
1515
                        addToResults(statement, file);
95✔
1516
                    },
1517
                    MethodStatement: (statement: MethodStatement) => {
1518
                        addToResults(statement, file);
3✔
1519
                    }
1520
                }), {
1521
                    walkMode: WalkMode.visitStatements
1522
                });
1523
            }
1524
        }
1525
        return [...results.values()];
39✔
1526
    }
1527

1528
    public getStatementsForXmlFile(scope: XmlScope, filterName?: string): FileLink<FunctionStatement>[] {
1529
        let results = new Map<Statement, FileLink<FunctionStatement>>();
14✔
1530
        const filesSearched = new Set<BrsFile>();
14✔
1531

1532
        //get all function names for the xml file and parents
1533
        let funcNames = new Set<string>();
14✔
1534
        let currentScope = scope;
14✔
1535
        while (isXmlScope(currentScope)) {
14✔
1536
            for (let name of currentScope.xmlFile.ast.componentElement.interfaceElement?.functions.map((f) => f.name) ?? []) {
20✔
1537
                if (!filterName || name === filterName) {
20!
1538
                    funcNames.add(name);
20✔
1539
                }
1540
            }
1541
            currentScope = currentScope.getParentScope() as XmlScope;
16✔
1542
        }
1543

1544
        //look through all files in scope for matches
1545
        for (const file of scope.getOwnFiles()) {
14✔
1546
            //skip non-brs files, or files we've already processed
1547
            if (!isBrsFile(file) || filesSearched.has(file)) {
28✔
1548
                continue;
14✔
1549
            }
1550
            filesSearched.add(file);
14✔
1551

1552
            file.ast.walk(createVisitor({
14✔
1553
                FunctionStatement: (statement: FunctionStatement) => {
1554
                    if (funcNames.has(statement.tokens.name.text)) {
19!
1555
                        if (!results.has(statement)) {
19!
1556
                            results.set(statement, { item: statement, file: file });
19✔
1557
                        }
1558
                    }
1559
                }
1560
            }), {
1561
                walkMode: WalkMode.visitStatements
1562
            });
1563
        }
1564
        return [...results.values()];
14✔
1565
    }
1566

1567
    /**
1568
     * Find all available completion items at the given position
1569
     * @param filePath can be a srcPath or a destPath
1570
     * @param position the position (line & column) where completions should be found
1571
     */
1572
    public getCompletions(filePath: string, position: Position) {
1573
        let file = this.getFile(filePath);
127✔
1574
        if (!file) {
127!
UNCOV
1575
            return [];
×
1576
        }
1577

1578
        const event: ProvideCompletionsEvent = {
127✔
1579
            program: this,
1580
            file: file,
1581
            scopes: this.getScopesForFile(file),
1582
            position: position,
1583
            completions: []
1584
        };
1585

1586
        this.plugins.emit('beforeProvideCompletions', event);
127✔
1587

1588
        this.plugins.emit('provideCompletions', event);
127✔
1589

1590
        this.plugins.emit('afterProvideCompletions', event);
127✔
1591

1592
        return event.completions;
127✔
1593
    }
1594

1595
    /**
1596
     * Goes through each file and builds a list of workspace symbols for the program. Used by LanguageServer's onWorkspaceSymbol functionality
1597
     */
1598
    public getWorkspaceSymbols() {
1599
        const event: ProvideWorkspaceSymbolsEvent = {
22✔
1600
            program: this,
1601
            workspaceSymbols: []
1602
        };
1603
        this.plugins.emit('beforeProvideWorkspaceSymbols', event);
22✔
1604
        this.plugins.emit('provideWorkspaceSymbols', event);
22✔
1605
        this.plugins.emit('afterProvideWorkspaceSymbols', event);
22✔
1606
        return event.workspaceSymbols;
22✔
1607
    }
1608

1609
    /**
1610
     * Given a position in a file, if the position is sitting on some type of identifier,
1611
     * go to the definition of that identifier (where this thing was first defined)
1612
     */
1613
    public getDefinition(srcPath: string, position: Position): Location[] {
1614
        let file = this.getFile(srcPath);
18✔
1615
        if (!file) {
18!
UNCOV
1616
            return [];
×
1617
        }
1618

1619
        const event: ProvideDefinitionEvent = {
18✔
1620
            program: this,
1621
            file: file,
1622
            position: position,
1623
            definitions: []
1624
        };
1625

1626
        this.plugins.emit('beforeProvideDefinition', event);
18✔
1627
        this.plugins.emit('provideDefinition', event);
18✔
1628
        this.plugins.emit('afterProvideDefinition', event);
18✔
1629
        return event.definitions;
18✔
1630
    }
1631

1632
    /**
1633
     * Get hover information for a file and position
1634
     */
1635
    public getHover(srcPath: string, position: Position): Hover[] {
1636
        let file = this.getFile(srcPath);
87✔
1637
        let result: Hover[];
1638
        if (file) {
87!
1639
            const event = {
87✔
1640
                program: this,
1641
                file: file,
1642
                position: position,
1643
                scopes: this.getScopesForFile(file),
1644
                hovers: []
1645
            } as ProvideHoverEvent;
1646
            this.plugins.emit('beforeProvideHover', event);
87✔
1647
            this.plugins.emit('provideHover', event);
87✔
1648
            this.plugins.emit('afterProvideHover', event);
87✔
1649
            result = event.hovers;
87✔
1650
        }
1651

1652
        return result ?? [];
87!
1653
    }
1654

1655
    /**
1656
     * Get full list of document symbols for a file
1657
     * @param srcPath path to the file
1658
     */
1659
    public getDocumentSymbols(srcPath: string): DocumentSymbol[] | undefined {
1660
        let file = this.getFile(srcPath);
24✔
1661
        if (file) {
24!
1662
            const event: ProvideDocumentSymbolsEvent = {
24✔
1663
                program: this,
1664
                file: file,
1665
                documentSymbols: []
1666
            };
1667
            this.plugins.emit('beforeProvideDocumentSymbols', event);
24✔
1668
            this.plugins.emit('provideDocumentSymbols', event);
24✔
1669
            this.plugins.emit('afterProvideDocumentSymbols', event);
24✔
1670
            return event.documentSymbols;
24✔
1671
        } else {
UNCOV
1672
            return undefined;
×
1673
        }
1674
    }
1675

1676
    /**
1677
     * Compute code actions for the given file and range
1678
     */
1679
    public getCodeActions(srcPath: string, range: Range) {
1680
        const codeActions = [] as CodeAction[];
16✔
1681
        const file = this.getFile(srcPath);
16✔
1682
        if (file) {
16✔
1683
            const fileUri = util.pathToUri(file?.srcPath);
15!
1684
            const diagnostics = this
15✔
1685
                //get all current diagnostics (filtered by diagnostic filters)
1686
                .getDiagnostics()
1687
                //only keep diagnostics related to this file
1688
                .filter(x => x.location?.uri === fileUri)
28!
1689
                //only keep diagnostics that touch this range
1690
                .filter(x => util.rangesIntersectOrTouch(x.location.range, range));
20✔
1691

1692
            const scopes = this.getScopesForFile(file);
15✔
1693

1694
            this.plugins.emit('onGetCodeActions', {
15✔
1695
                program: this,
1696
                file: file,
1697
                range: range,
1698
                diagnostics: diagnostics,
1699
                scopes: scopes,
1700
                codeActions: codeActions
1701
            });
1702
        }
1703
        return codeActions;
16✔
1704
    }
1705

1706
    /**
1707
     * Get semantic tokens for the specified file
1708
     */
1709
    public getSemanticTokens(srcPath: string): SemanticToken[] | undefined {
1710
        const file = this.getFile(srcPath);
26✔
1711
        if (file) {
26!
1712
            const result = [] as SemanticToken[];
26✔
1713
            this.plugins.emit('onGetSemanticTokens', {
26✔
1714
                program: this,
1715
                file: file,
1716
                scopes: this.getScopesForFile(file),
1717
                semanticTokens: result
1718
            });
1719
            return result;
26✔
1720
        }
1721
    }
1722

1723
    public getSignatureHelp(filePath: string, position: Position): SignatureInfoObj[] {
1724
        let file: BrsFile = this.getFile(filePath);
188✔
1725
        if (!file || !isBrsFile(file)) {
188✔
1726
            return [];
3✔
1727
        }
1728
        let callExpressionInfo = new CallExpressionInfo(file, position);
185✔
1729
        let signatureHelpUtil = new SignatureHelpUtil();
185✔
1730
        return signatureHelpUtil.getSignatureHelpItems(callExpressionInfo);
185✔
1731
    }
1732

1733
    public getReferences(srcPath: string, position: Position): Location[] {
1734
        //find the file
1735
        let file = this.getFile(srcPath);
4✔
1736

1737
        const event: ProvideReferencesEvent = {
4✔
1738
            program: this,
1739
            file: file,
1740
            position: position,
1741
            references: []
1742
        };
1743

1744
        this.plugins.emit('beforeProvideReferences', event);
4✔
1745
        this.plugins.emit('provideReferences', event);
4✔
1746
        this.plugins.emit('afterProvideReferences', event);
4✔
1747

1748
        return event.references;
4✔
1749
    }
1750

1751
    /**
1752
     * Transpile a single file and get the result as a string.
1753
     * This does not write anything to the file system.
1754
     *
1755
     * This should only be called by `LanguageServer`.
1756
     * Internal usage should call `_getTranspiledFileContents` instead.
1757
     * @param filePath can be a srcPath or a destPath
1758
     */
1759
    public async getTranspiledFileContents(filePath: string): Promise<FileTranspileResult> {
1760
        const file = this.getFile(filePath);
349✔
1761

1762
        return this.getTranspiledFileContentsPipeline.run(async () => {
349✔
1763

1764
            const result = {
349✔
1765
                destPath: file.destPath,
1766
                pkgPath: file.pkgPath,
1767
                srcPath: file.srcPath
1768
            } as FileTranspileResult;
1769

1770
            const expectedPkgPath = file.pkgPath.toLowerCase();
349✔
1771
            const expectedMapPath = `${expectedPkgPath}.map`;
349✔
1772
            const expectedTypedefPkgPath = expectedPkgPath.replace(/\.brs$/i, '.d.bs');
349✔
1773

1774
            //add a temporary plugin to tap into the file writing process
1775
            const plugin = this.plugins.addFirst({
349✔
1776
                name: 'getTranspiledFileContents',
1777
                beforeWriteFile: (event) => {
1778
                    const pkgPath = event.file.pkgPath.toLowerCase();
1,102✔
1779
                    switch (pkgPath) {
1,102✔
1780
                        //this is the actual transpiled file
1781
                        case expectedPkgPath:
1,102✔
1782
                            result.code = event.file.data.toString();
349✔
1783
                            break;
349✔
1784
                        //this is the sourcemap
1785
                        case expectedMapPath:
1786
                            result.map = event.file.data.toString();
193✔
1787
                            break;
193✔
1788
                        //this is the typedef
1789
                        case expectedTypedefPkgPath:
1790
                            result.typedef = event.file.data.toString();
9✔
1791
                            break;
9✔
1792
                        default:
1793
                        //no idea what this file is. just ignore it
1794
                    }
1795
                    //mark every file as processed so it they don't get written to the output directory
1796
                    event.processedFiles.add(event.file);
1,102✔
1797
                }
1798
            });
1799

1800
            try {
349✔
1801
                //now that the plugin has been registered, run the build with just this file
1802
                await this.build({
349✔
1803
                    files: [file]
1804
                });
1805
            } finally {
1806
                this.plugins.remove(plugin);
349✔
1807
            }
1808
            return result;
349✔
1809
        });
1810
    }
1811
    private getTranspiledFileContentsPipeline = new ActionPipeline();
2,123✔
1812

1813
    /**
1814
     * Get the absolute output path for a file
1815
     */
1816
    private getOutputPath(file: { pkgPath?: string }, outDir = this.getOutDir()) {
×
1817
        return s`${outDir}/${file.pkgPath}`;
2,005✔
1818
    }
1819

1820
    private getOutDir(outDir?: string) {
1821
        let result = outDir ?? this.options.outDir ?? this.options.outDir;
779!
1822
        if (!result) {
779!
UNCOV
1823
            result = rokuDeploy.getOptions(this.options as any).outDir;
×
1824
        }
1825
        result = s`${path.resolve(this.options.cwd ?? process.cwd(), result ?? '/')}`;
779!
1826
        return result;
779✔
1827
    }
1828

1829
    /**
1830
     * Prepare the program for building
1831
     * @param files the list of files that should be prepared
1832
     */
1833
    private async prepare(files: BscFile[]) {
1834
        const programEvent: PrepareProgramEvent = {
390✔
1835
            program: this,
1836
            editor: this.editor,
1837
            files: files
1838
        };
1839

1840
        //assign an editor to every file
1841
        for (const file of programEvent.files) {
390✔
1842
            //if the file doesn't have an editor yet, assign one now
1843
            if (!file.editor) {
791✔
1844
                file.editor = new Editor();
744✔
1845
            }
1846
        }
1847

1848
        //sort the entries to make transpiling more deterministic
1849
        programEvent.files.sort((a, b) => {
390✔
1850
            if (a.pkgPath < b.pkgPath) {
417✔
1851
                return -1;
355✔
1852
            } else if (a.pkgPath > b.pkgPath) {
62!
1853
                return 1;
62✔
1854
            } else {
UNCOV
1855
                return 1;
×
1856
            }
1857
        });
1858

1859
        await this.plugins.emitAsync('beforePrepareProgram', programEvent);
390✔
1860
        await this.plugins.emitAsync('prepareProgram', programEvent);
390✔
1861

1862
        const outDir = this.getOutDir();
390✔
1863

1864
        const entries: TranspileObj[] = [];
390✔
1865

1866
        for (const file of files) {
390✔
1867
            const scope = this.getFirstScopeForFile(file);
791✔
1868
            //link the symbol table for all the files in this scope
1869
            scope?.linkSymbolTable();
791✔
1870

1871
            //if the file doesn't have an editor yet, assign one now
1872
            if (!file.editor) {
791!
UNCOV
1873
                file.editor = new Editor();
×
1874
            }
1875
            const event = {
791✔
1876
                program: this,
1877
                file: file,
1878
                editor: file.editor,
1879
                scope: scope,
1880
                outputPath: this.getOutputPath(file, outDir)
1881
            } as PrepareFileEvent & { outputPath: string };
1882

1883
            await this.plugins.emitAsync('beforePrepareFile', event);
791✔
1884
            await this.plugins.emitAsync('prepareFile', event);
791✔
1885
            await this.plugins.emitAsync('afterPrepareFile', event);
791✔
1886

1887
            //TODO remove this in v1
1888
            entries.push(event);
791✔
1889

1890
            //unlink the symbolTable so the next loop iteration can link theirs
1891
            scope?.unlinkSymbolTable();
791✔
1892
        }
1893

1894
        await this.plugins.emitAsync('afterPrepareProgram', programEvent);
390✔
1895
        return files;
390✔
1896
    }
1897

1898
    /**
1899
     * Generate the contents of every file
1900
     */
1901
    private async serialize(files: BscFile[]) {
1902

1903
        const allFiles = new Map<BscFile, SerializedFile[]>();
389✔
1904

1905
        //exclude prunable files if that option is enabled
1906
        if (this.options.pruneEmptyCodeFiles === true) {
389✔
1907
            files = files.filter(x => x.canBePruned !== true);
9✔
1908
        }
1909

1910
        const serializeProgramEvent = await this.plugins.emitAsync('beforeSerializeProgram', {
389✔
1911
            program: this,
1912
            files: files,
1913
            result: allFiles
1914
        });
1915
        await this.plugins.emitAsync('onSerializeProgram', serializeProgramEvent);
389✔
1916

1917
        // serialize each file
1918
        for (const file of files) {
389✔
1919
            let scope = this.getFirstScopeForFile(file);
788✔
1920

1921
            //if the file doesn't have a scope, create a temporary scope for the file so it can depend on scope-level items
1922
            if (!scope) {
788✔
1923
                scope = new Scope(`temporary-for-${file.pkgPath}`, this);
401✔
1924
                scope.getAllFiles = () => [file];
3,595✔
1925
                scope.getOwnFiles = scope.getAllFiles;
401✔
1926
            }
1927

1928
            //link the symbol table for all the files in this scope
1929
            scope?.linkSymbolTable();
788!
1930
            const event: SerializeFileEvent = {
788✔
1931
                program: this,
1932
                file: file,
1933
                scope: scope,
1934
                result: allFiles
1935
            };
1936
            await this.plugins.emitAsync('beforeSerializeFile', event);
788✔
1937
            await this.plugins.emitAsync('serializeFile', event);
788✔
1938
            await this.plugins.emitAsync('afterSerializeFile', event);
788✔
1939
            //unlink the symbolTable so the next loop iteration can link theirs
1940
            scope?.unlinkSymbolTable();
788!
1941
        }
1942

1943
        this.plugins.emit('afterSerializeProgram', serializeProgramEvent);
389✔
1944

1945
        return allFiles;
389✔
1946
    }
1947

1948
    /**
1949
     * Write the entire project to disk
1950
     */
1951
    private async write(outDir: string, files: Map<BscFile, SerializedFile[]>) {
1952
        const programEvent = await this.plugins.emitAsync('beforeWriteProgram', {
389✔
1953
            program: this,
1954
            files: files,
1955
            outDir: outDir
1956
        });
1957
        //empty the out directory
1958
        await fsExtra.emptyDir(outDir);
389✔
1959

1960
        const serializedFiles = [...files]
389✔
1961
            .map(([, serializedFiles]) => serializedFiles)
788✔
1962
            .flat();
1963

1964
        //write all the files to disk (asynchronously)
1965
        await Promise.all(
389✔
1966
            serializedFiles.map(async (file) => {
1967
                const event = await this.plugins.emitAsync('beforeWriteFile', {
1,214✔
1968
                    program: this,
1969
                    file: file,
1970
                    outputPath: this.getOutputPath(file, outDir),
1971
                    processedFiles: new Set<SerializedFile>()
1972
                });
1973

1974
                await this.plugins.emitAsync('writeFile', event);
1,214✔
1975

1976
                await this.plugins.emitAsync('afterWriteFile', event);
1,214✔
1977
            })
1978
        );
1979

1980
        await this.plugins.emitAsync('afterWriteProgram', programEvent);
389✔
1981
    }
1982

1983
    private buildPipeline = new ActionPipeline();
2,123✔
1984

1985
    /**
1986
     * Build the project. This transpiles/transforms/copies all files and moves them to the staging directory
1987
     * @param options the list of options used to build the program
1988
     */
1989
    public async build(options?: ProgramBuildOptions) {
1990
        //run a single build at a time
1991
        await this.buildPipeline.run(async () => {
389✔
1992
            const outDir = this.getOutDir(options?.outDir);
389✔
1993

1994
            const event = await this.plugins.emitAsync('beforeBuildProgram', {
389✔
1995
                program: this,
1996
                editor: this.editor,
1997
                files: options?.files ?? Object.values(this.files)
2,334✔
1998
            });
1999

2000
            //prepare the program (and files) for building
2001
            event.files = await this.prepare(event.files);
389✔
2002

2003
            //stage the entire program
2004
            const serializedFilesByFile = await this.serialize(event.files);
389✔
2005

2006
            await this.write(outDir, serializedFilesByFile);
389✔
2007

2008
            await this.plugins.emitAsync('afterBuildProgram', event);
389✔
2009

2010
            //undo all edits for the program
2011
            this.editor.undoAll();
389✔
2012
            //undo all edits for each file
2013
            for (const file of event.files) {
389✔
2014
                file.editor.undoAll();
789✔
2015
            }
2016
        });
2017

2018
        this.logger.debug('Types Created', TypesCreated);
389✔
2019
        let totalTypesCreated = 0;
389✔
2020
        for (const key in TypesCreated) {
389✔
2021
            if (TypesCreated.hasOwnProperty(key)) {
11,662!
2022
                totalTypesCreated += TypesCreated[key];
11,662✔
2023

2024
            }
2025
        }
2026
        this.logger.info('Total Types Created', totalTypesCreated);
389✔
2027
    }
2028

2029
    /**
2030
     * Find a list of files in the program that have a function with the given name (case INsensitive)
2031
     */
2032
    public findFilesForFunction(functionName: string) {
2033
        const files = [] as BscFile[];
7✔
2034
        const lowerFunctionName = functionName.toLowerCase();
7✔
2035
        //find every file with this function defined
2036
        for (const file of Object.values(this.files)) {
7✔
2037
            if (isBrsFile(file)) {
25✔
2038
                //TODO handle namespace-relative function calls
2039
                //if the file has a function with this name
2040
                // eslint-disable-next-line @typescript-eslint/dot-notation
2041
                if (file['_cachedLookups'].functionStatementMap.get(lowerFunctionName)) {
17✔
2042
                    files.push(file);
2✔
2043
                }
2044
            }
2045
        }
2046
        return files;
7✔
2047
    }
2048

2049
    /**
2050
     * Find a list of files in the program that have a class with the given name (case INsensitive)
2051
     */
2052
    public findFilesForClass(className: string) {
2053
        const files = [] as BscFile[];
7✔
2054
        const lowerClassName = className.toLowerCase();
7✔
2055
        //find every file with this class defined
2056
        for (const file of Object.values(this.files)) {
7✔
2057
            if (isBrsFile(file)) {
25✔
2058
                //TODO handle namespace-relative classes
2059
                //if the file has a function with this name
2060

2061
                // eslint-disable-next-line @typescript-eslint/dot-notation
2062
                if (file['_cachedLookups'].classStatementMap.get(lowerClassName) !== undefined) {
17✔
2063
                    files.push(file);
1✔
2064
                }
2065
            }
2066
        }
2067
        return files;
7✔
2068
    }
2069

2070
    public findFilesForNamespace(name: string) {
2071
        const files = [] as BscFile[];
7✔
2072
        const lowerName = name.toLowerCase();
7✔
2073
        //find every file with this class defined
2074
        for (const file of Object.values(this.files)) {
7✔
2075
            if (isBrsFile(file)) {
25✔
2076

2077
                // eslint-disable-next-line @typescript-eslint/dot-notation
2078
                if (file['_cachedLookups'].namespaceStatements.find((x) => {
17✔
2079
                    const namespaceName = x.name.toLowerCase();
7✔
2080
                    return (
7✔
2081
                        //the namespace name matches exactly
2082
                        namespaceName === lowerName ||
9✔
2083
                        //the full namespace starts with the name (honoring the part boundary)
2084
                        namespaceName.startsWith(lowerName + '.')
2085
                    );
2086
                })) {
2087
                    files.push(file);
6✔
2088
                }
2089
            }
2090
        }
2091

2092
        return files;
7✔
2093
    }
2094

2095
    public findFilesForEnum(name: string) {
2096
        const files = [] as BscFile[];
8✔
2097
        const lowerName = name.toLowerCase();
8✔
2098
        //find every file with this enum defined
2099
        for (const file of Object.values(this.files)) {
8✔
2100
            if (isBrsFile(file)) {
26✔
2101
                // eslint-disable-next-line @typescript-eslint/dot-notation
2102
                if (file['_cachedLookups'].enumStatementMap.get(lowerName)) {
18✔
2103
                    files.push(file);
1✔
2104
                }
2105
            }
2106
        }
2107
        return files;
8✔
2108
    }
2109

2110
    private _manifest: Map<string, string>;
2111

2112
    /**
2113
     * Modify a parsed manifest map by reading `bs_const` and injecting values from `options.manifest.bs_const`
2114
     * @param parsedManifest The manifest map to read from and modify
2115
     */
2116
    private buildBsConstsIntoParsedManifest(parsedManifest: Map<string, string>) {
2117
        // Lift the bs_consts defined in the manifest
2118
        let bsConsts = getBsConst(parsedManifest, false);
428✔
2119

2120
        // Override or delete any bs_consts defined in the bs config
2121
        for (const key in this.options?.manifest?.bs_const) {
428!
2122
            const value = this.options.manifest.bs_const[key];
3✔
2123
            if (value === null) {
3✔
2124
                bsConsts.delete(key);
1✔
2125
            } else {
2126
                bsConsts.set(key, value);
2✔
2127
            }
2128
        }
2129

2130
        // convert the new list of bs consts back into a string for the rest of the down stream systems to use
2131
        let constString = '';
428✔
2132
        for (const [key, value] of bsConsts) {
428✔
2133
            constString += `${constString !== '' ? ';' : ''}${key}=${value.toString()}`;
413✔
2134
        }
2135

2136
        // Set the updated bs_const value
2137
        parsedManifest.set('bs_const', constString);
428✔
2138
    }
2139

2140
    /**
2141
     * Try to find and load the manifest into memory
2142
     * @param manifestFileObj A pointer to a potential manifest file object found during loading
2143
     * @param replaceIfAlreadyLoaded should we overwrite the internal `_manifest` if it already exists
2144
     */
2145
    public loadManifest(manifestFileObj?: FileObj, replaceIfAlreadyLoaded = true) {
1,727✔
2146
        //if we already have a manifest instance, and should not replace...then don't replace
2147
        if (!replaceIfAlreadyLoaded && this._manifest) {
1,746!
UNCOV
2148
            return;
×
2149
        }
2150
        let manifestPath = manifestFileObj
1,746✔
2151
            ? manifestFileObj.src
1,746✔
2152
            : path.join(this.options.rootDir, 'manifest');
2153

2154
        try {
1,746✔
2155
            // we only load this manifest once, so do it sync to improve speed downstream
2156
            const contents = fsExtra.readFileSync(manifestPath, 'utf-8');
1,746✔
2157
            const parsedManifest = parseManifest(contents);
428✔
2158
            this.buildBsConstsIntoParsedManifest(parsedManifest);
428✔
2159
            this._manifest = parsedManifest;
428✔
2160
        } catch (e) {
2161
            this._manifest = new Map();
1,318✔
2162
        }
2163
    }
2164

2165
    /**
2166
     * Get a map of the manifest information
2167
     */
2168
    public getManifest() {
2169
        if (!this._manifest) {
2,753✔
2170
            this.loadManifest();
1,726✔
2171
        }
2172
        return this._manifest;
2,753✔
2173
    }
2174

2175
    public dispose() {
2176
        this.plugins.emit('beforeProgramDispose', { program: this });
1,981✔
2177

2178
        for (let filePath in this.files) {
1,981✔
2179
            this.files[filePath]?.dispose?.();
2,506!
2180
        }
2181
        for (let name in this.scopes) {
1,981✔
2182
            this.scopes[name]?.dispose?.();
4,152!
2183
        }
2184
        this.globalScope?.dispose?.();
1,981!
2185
        this.dependencyGraph?.dispose?.();
1,981!
2186
    }
2187
}
2188

2189
export interface FileTranspileResult {
2190
    srcPath: string;
2191
    destPath: string;
2192
    pkgPath: string;
2193
    code: string;
2194
    map: string;
2195
    typedef: string;
2196
}
2197

2198

2199
class ProvideFileEventInternal<TFile extends BscFile = BscFile> implements ProvideFileEvent<TFile> {
2200
    constructor(
2201
        public program: Program,
2,838✔
2202
        public srcPath: string,
2,838✔
2203
        public destPath: string,
2,838✔
2204
        public data: LazyFileData,
2,838✔
2205
        public fileFactory: FileFactory
2,838✔
2206
    ) {
2207
        this.srcExtension = path.extname(srcPath)?.toLowerCase();
2,838!
2208
    }
2209

2210
    public srcExtension: string;
2211

2212
    public files: TFile[] = [];
2,838✔
2213
}
2214

2215
export interface ProgramBuildOptions {
2216
    /**
2217
     * The directory where the final built files should be placed. This directory will be cleared before running
2218
     */
2219
    outDir?: string;
2220
    /**
2221
     * An array of files to build. If omitted, the entire list of files from the program will be used instead.
2222
     * Typically you will want to leave this blank
2223
     */
2224
    files?: BscFile[];
2225
}
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