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

rokucommunity / brighterscript / #14045

20 Mar 2025 07:09PM UTC coverage: 87.156% (-0.007%) from 87.163%
#14045

push

web-flow
Merge e33b1f944 into 0eceb0830

13256 of 16072 branches covered (82.48%)

Branch coverage included in aggregate %.

1162 of 1279 new or added lines in 24 files covered. (90.85%)

233 existing lines in 7 files now uncovered.

14322 of 15570 relevant lines covered (91.98%)

21310.79 hits per line

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

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

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

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

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

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

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

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

96
        this.createGlobalScope();
1,936✔
97

98
        this.fileFactory = new FileFactory(this);
1,936✔
99
    }
100

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

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

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

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

120
        this.populateGlobalSymbolTable();
1,936✔
121
        this.globalScope.symbolTable.addSibling(this.componentsTable);
1,936✔
122

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

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

132

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

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

169
        const builtInSymbolData: ExtraSymbolData = { isBuiltIn: true };
1,936✔
170

171
        this.globalScope.symbolTable.addSymbol('boolean', builtInSymbolData, BooleanType.instance, SymbolTypeFlag.typetime);
1,936✔
172
        this.globalScope.symbolTable.addSymbol('double', builtInSymbolData, DoubleType.instance, SymbolTypeFlag.typetime);
1,936✔
173
        this.globalScope.symbolTable.addSymbol('dynamic', builtInSymbolData, DynamicType.instance, SymbolTypeFlag.typetime);
1,936✔
174
        this.globalScope.symbolTable.addSymbol('float', builtInSymbolData, FloatType.instance, SymbolTypeFlag.typetime);
1,936✔
175
        this.globalScope.symbolTable.addSymbol('function', builtInSymbolData, FunctionType.instance, SymbolTypeFlag.typetime);
1,936✔
176
        this.globalScope.symbolTable.addSymbol('integer', builtInSymbolData, IntegerType.instance, SymbolTypeFlag.typetime);
1,936✔
177
        this.globalScope.symbolTable.addSymbol('longinteger', builtInSymbolData, LongIntegerType.instance, SymbolTypeFlag.typetime);
1,936✔
178
        this.globalScope.symbolTable.addSymbol('object', builtInSymbolData, ObjectType.instance, SymbolTypeFlag.typetime);
1,936✔
179
        this.globalScope.symbolTable.addSymbol('string', builtInSymbolData, StringType.instance, SymbolTypeFlag.typetime);
1,936✔
180
        this.globalScope.symbolTable.addSymbol('void', builtInSymbolData, VoidType.instance, SymbolTypeFlag.typetime);
1,936✔
181

182
        BuiltInInterfaceAdder.getLookupTable = () => this.globalScope.symbolTable;
565,604✔
183

184
        for (const callable of globalCallables) {
1,936✔
185
            this.globalScope.symbolTable.addSymbol(callable.name, { ...builtInSymbolData, description: callable.shortDescription }, callable.type, SymbolTypeFlag.runtime);
151,008✔
186
        }
187

188
        for (const ifaceData of Object.values(interfaces) as BRSInterfaceData[]) {
1,936✔
189
            const nodeType = new InterfaceType(ifaceData.name);
170,368✔
190
            nodeType.addBuiltInInterfaces();
170,368✔
191
            nodeType.isBuiltIn = true;
170,368✔
192
            this.globalScope.symbolTable.addSymbol(ifaceData.name, { ...builtInSymbolData, description: ifaceData.description }, nodeType, SymbolTypeFlag.typetime);
170,368✔
193
        }
194

195
        for (const componentData of Object.values(components) as BRSComponentData[]) {
1,936✔
196
            const nodeType = new InterfaceType(componentData.name);
125,840✔
197
            nodeType.addBuiltInInterfaces();
125,840✔
198
            nodeType.isBuiltIn = true;
125,840✔
199
            if (componentData.name !== 'roSGNode') {
125,840✔
200
                // we will add `roSGNode` as shorthand for `roSGNodeNode`, since all roSgNode components are SceneGraph nodes
201
                this.globalScope.symbolTable.addSymbol(componentData.name, { ...builtInSymbolData, description: componentData.description }, nodeType, SymbolTypeFlag.typetime);
123,904✔
202
            }
203
        }
204

205
        for (const nodeData of Object.values(nodes) as SGNodeData[]) {
1,936✔
206
            this.recursivelyAddNodeToSymbolTable(nodeData);
185,856✔
207
        }
208

209
        for (const eventData of Object.values(events) as BRSEventData[]) {
1,936✔
210
            const nodeType = new InterfaceType(eventData.name);
34,848✔
211
            nodeType.addBuiltInInterfaces();
34,848✔
212
            nodeType.isBuiltIn = true;
34,848✔
213
            this.globalScope.symbolTable.addSymbol(eventData.name, { ...builtInSymbolData, description: eventData.description }, nodeType, SymbolTypeFlag.typetime);
34,848✔
214
        }
215

216
    }
217

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

226
    public diagnostics: DiagnosticManager;
227

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

234
    /**
235
     * Plugins which can provide extra diagnostics or transform AST
236
     */
237
    public plugins: PluginInterface;
238

239
    private fileSymbolInformation = new Map<string, { provides: ProvidedSymbolInfo; requires: UnresolvedSymbol[] }>();
1,936✔
240

241
    private currentScopeValidationOptions: ScopeValidationOptions;
242

243
    /**
244
     *  Map of typetime symbols which depend upon the key symbol
245
     */
246
    private symbolDependencies = new Map<string, Set<string>>();
1,936✔
247

248

249
    /**
250
     * Symbol Table for storing custom component types
251
     * This is a sibling to the global table (as Components can be used/referenced anywhere)
252
     * Keeping custom components out of the global table and in a specific symbol table
253
     * compartmentalizes their use
254
     */
255
    private componentsTable = new SymbolTable('Custom Components');
1,936✔
256

257
    public addFileSymbolInfo(file: BrsFile) {
258
        this.fileSymbolInformation.set(file.pkgPath, {
1,839✔
259
            provides: file.providedSymbols,
260
            requires: file.requiredSymbols
261
        });
262
    }
263

264
    public getFileSymbolInfo(file: BrsFile) {
265
        return this.fileSymbolInformation.get(file.pkgPath);
1,869✔
266
    }
267

268
    /**
269
     * The path to bslib.brs (the BrightScript runtime for certain BrighterScript features)
270
     */
271
    public get bslibPkgPath() {
272
        //if there's an aliased (preferred) version of bslib from roku_modules loaded into the program, use that
273
        if (this.getFile(bslibAliasedRokuModulesPkgPath)) {
2,474✔
274
            return bslibAliasedRokuModulesPkgPath;
11✔
275

276
            //if there's a non-aliased version of bslib from roku_modules, use that
277
        } else if (this.getFile(bslibNonAliasedRokuModulesPkgPath)) {
2,463✔
278
            return bslibNonAliasedRokuModulesPkgPath;
24✔
279

280
            //default to the embedded version
281
        } else {
282
            return `${this.options.bslibDestinationDir}${path.sep}bslib.brs`;
2,439✔
283
        }
284
    }
285

286
    public get bslibPrefix() {
287
        if (this.bslibPkgPath === bslibNonAliasedRokuModulesPkgPath) {
1,802✔
288
            return 'rokucommunity_bslib';
18✔
289
        } else {
290
            return 'bslib';
1,784✔
291
        }
292
    }
293

294

295
    /**
296
     * A map of every file loaded into this program, indexed by its original file location
297
     */
298
    public files = {} as Record<string, BscFile>;
1,936✔
299
    /**
300
     * A map of every file loaded into this program, indexed by its destPath
301
     */
302
    private destMap = new Map<string, BscFile>();
1,936✔
303
    /**
304
     * Plugins can contribute multiple virtual files for a single physical file.
305
     * This collection links the virtual files back to the physical file that produced them.
306
     * The key is the standardized and lower-cased srcPath
307
     */
308
    private fileClusters = new Map<string, BscFile[]>();
1,936✔
309

310
    private scopes = {} as Record<string, Scope>;
1,936✔
311

312
    protected addScope(scope: Scope) {
313
        this.scopes[scope.name] = scope;
2,107✔
314
        delete this.sortedScopeNames;
2,107✔
315
    }
316

317
    protected removeScope(scope: Scope) {
318
        if (this.scopes[scope.name]) {
16!
319
            delete this.scopes[scope.name];
16✔
320
            delete this.sortedScopeNames;
16✔
321
        }
322
    }
323

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

332
    /**
333
     * Get the component with the specified name
334
     */
335
    public getComponent(componentName: string) {
336
        if (componentName) {
3,033✔
337
            //return the first compoment in the list with this name
338
            //(components are ordered in this list by destPath to ensure consistency)
339
            return this.components[componentName.toLowerCase()]?.[0];
2,999✔
340
        } else {
341
            return undefined;
34✔
342
        }
343
    }
344

345
    /**
346
     * Get the sorted names of custom components
347
     */
348
    public getSortedComponentNames() {
349
        const componentNames = Object.keys(this.components);
1,496✔
350
        componentNames.sort((a, b) => {
1,496✔
351
            if (a < b) {
839✔
352
                return -1;
295✔
353
            } else if (b < a) {
544!
354
                return 1;
544✔
355
            }
UNCOV
356
            return 0;
×
357
        });
358
        return componentNames;
1,496✔
359
    }
360

361
    /**
362
     * Keeps a set of all the components that need to have their types updated during the current validation cycle
363
     * Map <componentKey, componentName>
364
     */
365
    private componentSymbolsToUpdate = new Map<string, string>();
1,936✔
366

367
    /**
368
     * Register (or replace) the reference to a component in the component map
369
     */
370
    private registerComponent(xmlFile: XmlFile, scope: XmlScope) {
371
        const key = this.getComponentKey(xmlFile);
446✔
372
        if (!this.components[key]) {
446✔
373
            this.components[key] = [];
429✔
374
        }
375
        this.components[key].push({
446✔
376
            file: xmlFile,
377
            scope: scope
378
        });
379
        this.components[key].sort((a, b) => {
446✔
380
            const pathA = a.file.destPath.toLowerCase();
5✔
381
            const pathB = b.file.destPath.toLowerCase();
5✔
382
            if (pathA < pathB) {
5✔
383
                return -1;
1✔
384
            } else if (pathA > pathB) {
4!
385
                return 1;
4✔
386
            }
UNCOV
387
            return 0;
×
388
        });
389
        this.syncComponentDependencyGraph(this.components[key]);
446✔
390
        this.addDeferredComponentTypeSymbolCreation(xmlFile);
446✔
391
    }
392

393
    /**
394
     * Remove the specified component from the components map
395
     */
396
    private unregisterComponent(xmlFile: XmlFile) {
397
        const key = this.getComponentKey(xmlFile);
16✔
398
        const arr = this.components[key] || [];
16!
399
        for (let i = 0; i < arr.length; i++) {
16✔
400
            if (arr[i].file === xmlFile) {
16!
401
                arr.splice(i, 1);
16✔
402
                break;
16✔
403
            }
404
        }
405

406
        this.syncComponentDependencyGraph(arr);
16✔
407
        this.addDeferredComponentTypeSymbolCreation(xmlFile);
16✔
408
    }
409

410
    /**
411
     * Adds a component described in an XML to the set of components that needs to be updated this validation cycle.
412
     * @param xmlFile XML file with <component> tag
413
     */
414
    private addDeferredComponentTypeSymbolCreation(xmlFile: XmlFile) {
415
        const componentKey = this.getComponentKey(xmlFile);
1,480✔
416
        const componentName = xmlFile.componentName?.text;
1,480✔
417
        if (this.componentSymbolsToUpdate.has(componentKey)) {
1,480✔
418
            return;
903✔
419
        }
420
        this.componentSymbolsToUpdate.set(componentKey, componentName);
577✔
421
    }
422

423
    private getComponentKey(xmlFile: XmlFile) {
424
        return (xmlFile.componentName?.text ?? xmlFile.pkgPath).toLowerCase();
1,942✔
425
    }
426

427
    /**
428
     * Resolves symbol table with the first component in this.components to have the same name as the component in the file
429
     * @param componentKey key getting a component from `this.components`
430
     * @param componentName the unprefixed name of the component that will be added (e.g. 'MyLabel' NOT 'roSgNodeMyLabel')
431
     */
432
    private updateComponentSymbolInGlobalScope(componentKey: string, componentName: string) {
433
        const symbolName = componentName ? util.getSgNodeTypeName(componentName) : undefined;
529✔
434
        if (!symbolName) {
529✔
435
            return;
7✔
436
        }
437
        const components = this.components[componentKey] || [];
522!
438
        const previousComponentType = this.componentsTable.getSymbolType(symbolName, { flags: SymbolTypeFlag.typetime });
522✔
439
        // Remove any existing symbols that match
440
        this.componentsTable.removeSymbol(symbolName);
522✔
441
        if (components.length > 0) {
522✔
442
            // There is a component that can be added - use it.
443
            const componentScope = components[0].scope;
521✔
444

445
            this.componentsTable.removeSymbol(symbolName);
521✔
446
            componentScope.linkSymbolTable();
521✔
447
            const componentType = componentScope.getComponentType();
521✔
448
            if (componentType) {
521!
449
                this.componentsTable.addSymbol(symbolName, {}, componentType, SymbolTypeFlag.typetime);
521✔
450
            }
451
            const typeData = {};
521✔
452
            const isSameAsPrevious = previousComponentType && componentType.isEqual(previousComponentType, typeData);
521✔
453
            const isComponentTypeDifferent = !previousComponentType || isReferenceType(previousComponentType) || !isSameAsPrevious;
521✔
454
            componentScope.unlinkSymbolTable();
521✔
455
            return isComponentTypeDifferent;
521✔
456

457
        }
458
        // There was a previous component type, but no new one, so it's different
459
        return !!previousComponentType;
1✔
460
    }
461

462
    /**
463
     * 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
464
     * This is so on a first validation, these types can be resolved in teh future (eg. when the actual component is created)
465
     * If we don't add reference types at this top level, they will be created at the file level, and will never get resolved
466
     * @param componentKey key getting a component from `this.components`
467
     * @param componentName the unprefixed name of the component that will be added (e.g. 'MyLabel' NOT 'roSgNodeMyLabel')
468
     */
469
    private addComponentReferenceType(componentKey: string, componentName: string) {
470
        const symbolName = componentName ? util.getSgNodeTypeName(componentName) : undefined;
529✔
471
        if (!symbolName) {
529✔
472
            return;
7✔
473
        }
474
        const components = this.components[componentKey] || [];
522!
475

476
        if (components.length > 0) {
522✔
477
            // There is a component that can be added,
478
            if (!this.componentsTable.hasSymbol(symbolName, SymbolTypeFlag.typetime)) {
521✔
479
                // it doesn't already exist in the table
480
                const componentRefType = new ReferenceType(symbolName, symbolName, SymbolTypeFlag.typetime, () => this.componentsTable);
3,330✔
481
                if (componentRefType) {
375!
482
                    this.componentsTable.addSymbol(symbolName, {}, componentRefType, SymbolTypeFlag.typetime);
375✔
483
                }
484
            }
485
        } else {
486
            // there is no component. remove from table
487
            this.componentsTable.removeSymbol(symbolName);
1✔
488
        }
489
    }
490

491
    /**
492
     * re-attach the dependency graph with a new key for any component who changed
493
     * their position in their own named array (only matters when there are multiple
494
     * components with the same name)
495
     */
496
    private syncComponentDependencyGraph(components: Array<{ file: XmlFile; scope: XmlScope }>) {
497
        //reattach every dependency graph
498
        for (let i = 0; i < components.length; i++) {
462✔
499
            const { file, scope } = components[i];
452✔
500

501
            //attach (or re-attach) the dependencyGraph for every component whose position changed
502
            if (file.dependencyGraphIndex !== i) {
452✔
503
                file.dependencyGraphIndex = i;
448✔
504
                this.dependencyGraph.addOrReplace(file.dependencyGraphKey, file.dependencies);
448✔
505
                file.attachDependencyGraph(this.dependencyGraph);
448✔
506
                scope.attachDependencyGraph(this.dependencyGraph);
448✔
507
            }
508
        }
509
    }
510

511
    /**
512
     * Get a list of all files that are included in the project but are not referenced
513
     * by any scope in the program.
514
     */
515
    public getUnreferencedFiles() {
UNCOV
516
        let result = [] as BscFile[];
×
517
        for (let filePath in this.files) {
×
518
            let file = this.files[filePath];
×
519
            //is this file part of a scope
UNCOV
520
            if (!this.getFirstScopeForFile(file)) {
×
521
                //no scopes reference this file. add it to the list
UNCOV
522
                result.push(file);
×
523
            }
524
        }
UNCOV
525
        return result;
×
526
    }
527

528
    /**
529
     * Get the list of errors for the entire program.
530
     */
531
    public getDiagnostics() {
532
        return this.diagnostics.getDiagnostics();
1,227✔
533
    }
534

535
    /**
536
     * Determine if the specified file is loaded in this program right now.
537
     * @param filePath the absolute or relative path to the file
538
     * @param normalizePath should the provided path be normalized before use
539
     */
540
    public hasFile(filePath: string, normalizePath = true) {
2,866✔
541
        return !!this.getFile(filePath, normalizePath);
2,866✔
542
    }
543

544
    /**
545
     * roku filesystem is case INsensitive, so find the scope by key case insensitive
546
     * @param scopeName xml scope names are their `destPath`. Source scope is stored with the key `"source"`
547
     */
548
    public getScopeByName(scopeName: string): Scope | undefined {
549
        if (!scopeName) {
61!
UNCOV
550
            return undefined;
×
551
        }
552
        //most scopes are xml file pkg paths. however, the ones that are not are single names like "global" and "scope",
553
        //so it's safe to run the standardizePkgPath method
554
        scopeName = s`${scopeName}`;
61✔
555
        let key = Object.keys(this.scopes).find(x => x.toLowerCase() === scopeName.toLowerCase());
139✔
556
        return this.scopes[key!];
61✔
557
    }
558

559
    /**
560
     * Return all scopes
561
     */
562
    public getScopes() {
563
        return Object.values(this.scopes);
13✔
564
    }
565

566
    /**
567
     * Find the scope for the specified component
568
     */
569
    public getComponentScope(componentName: string) {
570
        return this.getComponent(componentName)?.scope;
929✔
571
    }
572

573
    /**
574
     * Update internal maps with this file reference
575
     */
576
    private assignFile<T extends BscFile = BscFile>(file: T) {
577
        const fileAddEvent: BeforeFileAddEvent = {
2,607✔
578
            file: file,
579
            program: this
580
        };
581

582
        this.plugins.emit('beforeFileAdd', fileAddEvent);
2,607✔
583

584
        this.files[file.srcPath.toLowerCase()] = file;
2,607✔
585
        this.destMap.set(file.destPath.toLowerCase(), file);
2,607✔
586

587
        this.plugins.emit('afterFileAdd', fileAddEvent);
2,607✔
588

589
        return file;
2,607✔
590
    }
591

592
    /**
593
     * Remove this file from internal maps
594
     */
595
    private unassignFile<T extends BscFile = BscFile>(file: T) {
596
        delete this.files[file.srcPath.toLowerCase()];
215✔
597
        this.destMap.delete(file.destPath.toLowerCase());
215✔
598
        return file;
215✔
599
    }
600

601
    /**
602
     * Load a file into the program. If that file already exists, it is replaced.
603
     * If file contents are provided, those are used, Otherwise, the file is loaded from the file system
604
     * @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:/`)
605
     * @param fileData the file contents. omit or pass `undefined` to prevent loading the data at this time
606
     */
607
    public setFile<T extends BscFile>(srcDestOrPkgPath: string, fileData?: FileData): T;
608
    /**
609
     * Load a file into the program. If that file already exists, it is replaced.
610
     * @param fileEntry an object that specifies src and dest for the file.
611
     * @param fileData the file contents. omit or pass `undefined` to prevent loading the data at this time
612
     */
613
    public setFile<T extends BscFile>(fileEntry: FileObj, fileData: FileData): T;
614
    public setFile<T extends BscFile>(fileParam: FileObj | string, fileData: FileData): T {
615
        //normalize the file paths
616
        const { srcPath, destPath } = this.getPaths(fileParam, this.options.rootDir);
2,603✔
617

618
        let file = this.logger.time(LogLevel.debug, ['Program.setFile()', chalk.green(srcPath)], () => {
2,603✔
619
            //if the file is already loaded, remove it
620
            if (this.hasFile(srcPath)) {
2,603✔
621
                this.removeFile(srcPath, true, true);
195✔
622
            }
623

624
            const data = new LazyFileData(fileData);
2,603✔
625

626
            const event = new ProvideFileEventInternal(this, srcPath, destPath, data, this.fileFactory);
2,603✔
627

628
            this.plugins.emit('beforeProvideFile', event);
2,603✔
629
            this.plugins.emit('provideFile', event);
2,603✔
630
            this.plugins.emit('afterProvideFile', event);
2,603✔
631

632
            //if no files were provided, create a AssetFile to represent it.
633
            if (event.files.length === 0) {
2,603✔
634
                event.files.push(
24✔
635
                    this.fileFactory.AssetFile({
636
                        srcPath: event.srcPath,
637
                        destPath: event.destPath,
638
                        pkgPath: event.destPath,
639
                        data: data
640
                    })
641
                );
642
            }
643

644
            //find the file instance for the srcPath that triggered this action.
645
            const primaryFile = event.files.find(x => x.srcPath === srcPath);
2,603✔
646

647
            if (!primaryFile) {
2,603!
UNCOV
648
                throw new Error(`No file provided for srcPath '${srcPath}'. Instead, received ${JSON.stringify(event.files.map(x => ({
×
649
                    type: x.type,
650
                    srcPath: x.srcPath,
651
                    destPath: x.destPath
652
                })))}`);
653
            }
654

655
            //link the virtual files to the primary file
656
            this.fileClusters.set(primaryFile.srcPath?.toLowerCase(), event.files);
2,603!
657

658
            for (const file of event.files) {
2,603✔
659
                file.srcPath = s(file.srcPath);
2,607✔
660
                if (file.destPath) {
2,607!
661
                    file.destPath = s`${util.replaceCaseInsensitive(file.destPath, this.options.rootDir, '')}`;
2,607✔
662
                }
663
                if (file.pkgPath) {
2,607✔
664
                    file.pkgPath = s`${util.replaceCaseInsensitive(file.pkgPath, this.options.rootDir, '')}`;
2,603✔
665
                } else {
666
                    file.pkgPath = file.destPath;
4✔
667
                }
668
                file.excludeFromOutput = file.excludeFromOutput === true;
2,607✔
669

670
                //set the dependencyGraph key for every file to its destPath
671
                file.dependencyGraphKey = file.destPath.toLowerCase();
2,607✔
672

673
                this.assignFile(file);
2,607✔
674

675
                //register a callback anytime this file's dependencies change
676
                if (typeof file.onDependenciesChanged === 'function') {
2,607✔
677
                    file.disposables ??= [];
2,575!
678
                    file.disposables.push(
2,575✔
679
                        this.dependencyGraph.onchange(file.dependencyGraphKey, file.onDependenciesChanged.bind(file))
680
                    );
681
                }
682

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

686
                //if this is a `source` file, add it to the source scope's dependency list
687
                if (this.isSourceBrsFile(file)) {
2,607✔
688
                    this.createSourceScope();
1,730✔
689
                    this.dependencyGraph.addDependency('scope:source', file.dependencyGraphKey);
1,730✔
690
                }
691

692
                //if this is an xml file in the components folder, register it as a component
693
                if (this.isComponentsXmlFile(file)) {
2,607✔
694
                    //create a new scope for this xml file
695
                    let scope = new XmlScope(file, this);
446✔
696
                    this.addScope(scope);
446✔
697

698
                    //register this componet now that we have parsed it and know its component name
699
                    this.registerComponent(file, scope);
446✔
700

701
                    //notify plugins that the scope is created and the component is registered
702
                    this.plugins.emit('afterScopeCreate', {
446✔
703
                        program: this,
704
                        scope: scope
705
                    });
706
                }
707
            }
708

709
            return primaryFile;
2,603✔
710
        });
711
        return file as T;
2,603✔
712
    }
713

714
    /**
715
     * Given a srcPath, a destPath, or both, resolve whichever is missing, relative to rootDir.
716
     * @param fileParam an object representing file paths
717
     * @param rootDir must be a pre-normalized path
718
     */
719
    private getPaths(fileParam: string | FileObj | { srcPath?: string; pkgPath?: string }, rootDir: string) {
720
        let srcPath: string | undefined;
721
        let destPath: string | undefined;
722

723
        assert.ok(fileParam, 'fileParam is required');
2,821✔
724

725
        //lift the path vars from the incoming param
726
        if (typeof fileParam === 'string') {
2,821✔
727
            fileParam = this.removePkgPrefix(fileParam);
2,350✔
728
            srcPath = s`${path.resolve(rootDir, fileParam)}`;
2,350✔
729
            destPath = s`${util.replaceCaseInsensitive(srcPath, rootDir, '')}`;
2,350✔
730
        } else {
731
            let param: any = fileParam;
471✔
732

733
            if (param.src) {
471✔
734
                srcPath = s`${param.src}`;
470✔
735
            }
736
            if (param.srcPath) {
471!
UNCOV
737
                srcPath = s`${param.srcPath}`;
×
738
            }
739
            if (param.dest) {
471✔
740
                destPath = s`${this.removePkgPrefix(param.dest)}`;
470✔
741
            }
742
            if (param.pkgPath) {
471!
UNCOV
743
                destPath = s`${this.removePkgPrefix(param.pkgPath)}`;
×
744
            }
745
        }
746

747
        //if there's no srcPath, use the destPath to build an absolute srcPath
748
        if (!srcPath) {
2,821✔
749
            srcPath = s`${rootDir}/${destPath}`;
1✔
750
        }
751
        //coerce srcPath to an absolute path
752
        if (!path.isAbsolute(srcPath)) {
2,821✔
753
            srcPath = util.standardizePath(srcPath);
1✔
754
        }
755

756
        //if destPath isn't set, compute it from the other paths
757
        if (!destPath) {
2,821✔
758
            destPath = s`${util.replaceCaseInsensitive(srcPath, rootDir, '')}`;
1✔
759
        }
760

761
        assert.ok(srcPath, 'fileEntry.src is required');
2,821✔
762
        assert.ok(destPath, 'fileEntry.dest is required');
2,821✔
763

764
        return {
2,821✔
765
            srcPath: srcPath,
766
            //remove leading slash
767
            destPath: destPath.replace(/^[\/\\]+/, '')
768
        };
769
    }
770

771
    /**
772
     * Remove any leading `pkg:/` found in the path
773
     */
774
    private removePkgPrefix(path: string) {
775
        return path.replace(/^pkg:\//i, '');
2,820✔
776
    }
777

778
    /**
779
     * Is this file a .brs file found somewhere within the `pkg:/source/` folder?
780
     */
781
    private isSourceBrsFile(file: BscFile) {
782
        return !!/^(pkg:\/)?source[\/\\]/.exec(file.destPath);
2,822✔
783
    }
784

785
    /**
786
     * Is this file a .brs file found somewhere within the `pkg:/source/` folder?
787
     */
788
    private isComponentsXmlFile(file: BscFile): file is XmlFile {
789
        return isXmlFile(file) && !!/^(pkg:\/)?components[\/\\]/.exec(file.destPath);
2,607✔
790
    }
791

792
    /**
793
     * Ensure source scope is created.
794
     * Note: automatically called internally, and no-op if it exists already.
795
     */
796
    public createSourceScope() {
797
        if (!this.scopes.source) {
2,543✔
798
            const sourceScope = new Scope('source', this, 'scope:source');
1,661✔
799
            sourceScope.attachDependencyGraph(this.dependencyGraph);
1,661✔
800
            this.addScope(sourceScope);
1,661✔
801
            this.plugins.emit('afterScopeCreate', {
1,661✔
802
                program: this,
803
                scope: sourceScope
804
            });
805
        }
806
    }
807

808
    /**
809
     * Remove a set of files from the program
810
     * @param srcPaths can be an array of srcPath or destPath strings
811
     * @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
812
     */
813
    public removeFiles(srcPaths: string[], normalizePath = true) {
1✔
814
        for (let srcPath of srcPaths) {
1✔
815
            this.removeFile(srcPath, normalizePath);
1✔
816
        }
817
    }
818

819
    /**
820
     * Remove a file from the program
821
     * @param filePath can be a srcPath, a destPath, or a destPath with leading `pkg:/`
822
     * @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
823
     */
824
    public removeFile(filePath: string, normalizePath = true, keepSymbolInformation = false) {
32✔
825
        this.logger.debug('Program.removeFile()', filePath);
213✔
826
        const paths = this.getPaths(filePath, this.options.rootDir);
213✔
827

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

831
        for (const file of files) {
213✔
832
            //if a file has already been removed, nothing more needs to be done here
833
            if (!file || !this.hasFile(file.srcPath)) {
216✔
834
                continue;
1✔
835
            }
836
            this.diagnostics.clearForFile(file.srcPath);
215✔
837

838
            const event: BeforeFileRemoveEvent = { file: file, program: this };
215✔
839
            this.plugins.emit('beforeFileRemove', event);
215✔
840

841
            //if there is a scope named the same as this file's path, remove it (i.e. xml scopes)
842
            let scope = this.scopes[file.destPath];
215✔
843
            if (scope) {
215✔
844
                this.logger.debug('Removing associated scope', scope.name);
16✔
845
                const scopeDisposeEvent = {
16✔
846
                    program: this,
847
                    scope: scope
848
                };
849
                this.plugins.emit('beforeScopeDispose', scopeDisposeEvent);
16✔
850
                this.plugins.emit('onScopeDispose', scopeDisposeEvent);
16✔
851
                scope.dispose();
16✔
852
                //notify dependencies of this scope that it has been removed
853
                this.dependencyGraph.remove(scope.dependencyGraphKey!);
16✔
854
                this.removeScope(this.scopes[file.destPath]);
16✔
855
                this.plugins.emit('afterScopeDispose', scopeDisposeEvent);
16✔
856
            }
857
            //remove the file from the program
858
            this.unassignFile(file);
215✔
859

860
            this.dependencyGraph.remove(file.dependencyGraphKey);
215✔
861

862
            //if this is a pkg:/source file, notify the `source` scope that it has changed
863
            if (this.isSourceBrsFile(file)) {
215✔
864
                this.dependencyGraph.removeDependency('scope:source', file.dependencyGraphKey);
144✔
865
            }
866
            if (isBrsFile(file)) {
215✔
867
                this.logger.debug('Removing file symbol info', file.srcPath);
192✔
868

869
                if (!keepSymbolInformation) {
192✔
870
                    this.fileSymbolInformation.delete(file.pkgPath);
12✔
871
                }
872
                this.crossScopeValidation.clearResolutionsForFile(file);
192✔
873
            }
874

875
            this.diagnostics.clearForFile(file.srcPath);
215✔
876

877
            //if this is a component, remove it from our components map
878
            if (isXmlFile(file)) {
215✔
879
                this.logger.debug('Unregistering component', file.srcPath);
16✔
880

881
                this.unregisterComponent(file);
16✔
882
            }
883
            this.logger.debug('Disposing file', file.srcPath);
215✔
884

885
            //dispose any disposable things on the file
886
            for (const disposable of file?.disposables ?? []) {
215!
887
                disposable();
208✔
888
            }
889
            //dispose file
890
            file?.dispose?.();
215!
891

892
            this.plugins.emit('afterFileRemove', event);
215✔
893
        }
894
    }
895

896
    public crossScopeValidation = new CrossScopeValidator(this);
1,936✔
897

898
    private isFirstValidation = true;
1,936✔
899

900
    /**
901
     * Counter used to track which validation run is being logged
902
     */
903
    private validationRunSequence = 1;
1,936✔
904

905
    /**
906
     * How many milliseconds can pass while doing synchronous operations in validate before we register a short timeout (i.e. yield to the event loop)
907
     */
908
    private validationMinSyncDuration = 75;
1,936✔
909

910
    private validatePromise: Promise<void> | undefined;
911

912
    /**
913
     * Traverse the entire project, and validate all scopes
914
     */
915
    public validate(): void;
916
    public validate(options: { async: false; cancellationToken?: CancellationToken }): void;
917
    public validate(options: { async: true; cancellationToken?: CancellationToken }): Promise<void>;
918
    public validate(options?: { async?: boolean; cancellationToken?: CancellationToken }) {
919
        const validationRunId = this.validationRunSequence++;
1,502✔
920

921
        let previousValidationPromise = this.validatePromise;
1,502✔
922
        const deferred = new Deferred();
1,502✔
923

924
        if (options?.async) {
1,502✔
925
            //we're async, so create a new promise chain to resolve after this validation is done
926
            this.validatePromise = Promise.resolve(previousValidationPromise).then(() => {
124✔
927
                return deferred.promise;
124✔
928
            });
929

930
            //we are not async but there's a pending promise, then we cannot run this validation
931
        } else if (previousValidationPromise !== undefined) {
1,378!
NEW
932
            throw new Error('Cannot run synchronous validation while an async validation is in progress');
×
933
        }
934

935
        let beforeProgramValidateWasEmitted = false;
1,502✔
936

937
        //validate every file
938
        const brsFilesValidated: BrsFile[] = [];
1,502✔
939
        const xmlFilesValidated: XmlFile[] = [];
1,502✔
940
        const changedSymbols = new Map<SymbolTypeFlag, Set<string>>();
1,502✔
941
        const changedComponentTypes: string[] = [];
1,502✔
942

943
        let logValidateEnd = (status?: string) => { };
1,502✔
944

945
        //will be populated later on during the correspnding sequencer event
946
        let filesToProcess: BscFile[];
947
        let filesToBeValidatedInScopeContext: Set<BscFile>;
948

949
        const sequencer = new Sequencer({
1,502✔
950
            name: 'program.validate',
951
            cancellationToken: options?.cancellationToken ?? new CancellationTokenSource().token,
9,012✔
952
            minSyncDuration: this.validationMinSyncDuration
953
        });
954
        //this sequencer allows us to run in both sync and async mode, depending on whether options.async is enabled.
955
        //We use this to prevent starving the CPU during long validate cycles when running in a language server context
956
        sequencer
1,502✔
957
            .once(() => {
958
                //if running in async mode, return the previous validation promise to ensure we're only running one at a time
959
                if (options?.async) {
1,502✔
960
                    return previousValidationPromise;
124✔
961
                }
962
            })
963
            .once(() => {
964
                logValidateEnd = this.logger.timeStart(LogLevel.log, `Validating project${(this.logger.logLevel as LogLevel) > LogLevel.log ? ` (run ${validationRunId})` : ''}`);
1,500!
965
                this.diagnostics.clearForTag(ProgramValidatorDiagnosticsTag);
1,500✔
966
                this.plugins.emit('beforeProgramValidate', {
1,500✔
967
                    program: this
968
                });
969
                beforeProgramValidateWasEmitted = true;
1,500✔
970
                this.plugins.emit('onProgramValidate', {
1,500✔
971
                    program: this
972
                });
973
            })
974
            //handle some component symbol stuff
975
            .forEach(
976
                //return the list of files that need to be processed
977
                () => {
978
                    filesToProcess = Object.values(this.files).sort(firstBy(x => x.srcPath)).filter(x => !x.isValidated);
4,274✔
979
                    return filesToProcess;
1,500✔
980
                },
981
                (file) => {
982
                    // cast a wide net for potential changes in components
983
                    if (isXmlFile(file)) {
2,175✔
984
                        this.addDeferredComponentTypeSymbolCreation(file);
392✔
985
                    } else if (isBrsFile(file)) {
1,783!
986
                        for (const scope of this.getScopesForFile(file)) {
1,783✔
987
                            if (isXmlScope(scope)) {
2,059✔
988
                                this.addDeferredComponentTypeSymbolCreation(scope.xmlFile);
626✔
989
                            }
990
                        }
991
                    }
992
                },
993
                { label: 'Prebuild component types' }
994
            )
995
            .once(() => {
996
                // Create reference component types for any component that changes
997
                for (let [componentKey, componentName] of this.componentSymbolsToUpdate.entries()) {
1,499✔
998
                    this.addComponentReferenceType(componentKey, componentName);
529✔
999
                }
1000
            }, { label: 'Prebuild component types' })
1001

1002
            //run the beforeEach event for every unvalidated file
1003
            .forEach(() => filesToProcess, (file) => {
1,499✔
1004
                this.plugins.emit('beforeFileValidate', {
2,175✔
1005
                    program: this,
1006
                    file: file
1007
                });
1008
            })
1009
            //validate each unvalidated file
1010
            .forEach(() => filesToProcess, (file) => {
1,496✔
1011
                this.plugins.emit('onFileValidate', {
2,174✔
1012
                    program: this,
1013
                    file: file
1014
                });
1015
                file.isValidated = true;
2,174✔
1016
                if (isBrsFile(file)) {
2,174✔
1017
                    brsFilesValidated.push(file);
1,782✔
1018
                } else if (isXmlFile(file)) {
392!
1019
                    xmlFilesValidated.push(file);
392✔
1020
                }
1021
            })
1022
            //handle afterFileValidate events
1023
            .forEach(() => filesToProcess, (file) => {
1,496✔
1024
                this.plugins.emit('afterFileValidate', {
2,174✔
1025
                    program: this,
1026
                    file: file
1027
                });
1028
            })
1029
            .once(() => {
1030
                // Build component types for any component that changes
1031
                this.logger.time(LogLevel.info, ['Build component types'], () => {
1,496✔
1032
                    for (let [componentKey, componentName] of this.componentSymbolsToUpdate.entries()) {
1,496✔
1033
                        if (this.updateComponentSymbolInGlobalScope(componentKey, componentName)) {
529✔
1034
                            changedComponentTypes.push(util.getSgNodeTypeName(componentName).toLowerCase());
383✔
1035
                        }
1036
                    }
1037
                    this.componentSymbolsToUpdate.clear();
1,496✔
1038
                });
1039
            })
1040
            .once(() => {
1041
                const changedSymbolsMapArr = [...brsFilesValidated, ...xmlFilesValidated]?.map(f => {
1,496!
1042
                    if (isBrsFile(f)) {
2,174✔
1043
                        return f.providedSymbols.changes;
1,782✔
1044
                    }
1045
                    return null;
392✔
1046
                }).filter(x => x);
2,174✔
1047

1048
                // update the map of typetime dependencies
1049
                for (const file of brsFilesValidated) {
1,496✔
1050
                    for (const [symbolName, provided] of file.providedSymbols.symbolMap.get(SymbolTypeFlag.typetime).entries()) {
1,782✔
1051
                        // clear existing dependencies
1052
                        for (const values of this.symbolDependencies.values()) {
669✔
1053
                            values.delete(symbolName);
62✔
1054
                        }
1055

1056
                        // map types to the set of types that depend upon them
1057
                        for (const dependentSymbol of provided.requiredSymbolNames?.values() ?? []) {
669!
1058
                            const dependentSymbolLower = dependentSymbol.toLowerCase();
185✔
1059
                            if (!this.symbolDependencies.has(dependentSymbolLower)) {
185✔
1060
                                this.symbolDependencies.set(dependentSymbolLower, new Set<string>());
163✔
1061
                            }
1062
                            const symbolsDependentUpon = this.symbolDependencies.get(dependentSymbolLower);
185✔
1063
                            symbolsDependentUpon.add(symbolName);
185✔
1064
                        }
1065
                    }
1066
                }
1067

1068
                for (const flag of [SymbolTypeFlag.runtime, SymbolTypeFlag.typetime]) {
1,496✔
1069
                    const changedSymbolsSetArr = changedSymbolsMapArr.map(symMap => symMap.get(flag));
3,564✔
1070
                    const changedSymbolSet = new Set<string>();
2,992✔
1071
                    for (const changeSet of changedSymbolsSetArr) {
2,992✔
1072
                        for (const change of changeSet) {
3,564✔
1073
                            changedSymbolSet.add(change);
3,554✔
1074
                        }
1075
                    }
1076
                    changedSymbols.set(flag, changedSymbolSet);
2,992✔
1077
                }
1078

1079
                // update changed symbol set with any changed component
1080
                for (const changedComponentType of changedComponentTypes) {
1,496✔
1081
                    changedSymbols.get(SymbolTypeFlag.typetime).add(changedComponentType);
383✔
1082
                }
1083

1084
                // Add any additional types that depend on a changed type
1085
                // as each iteration of the loop might add new types, need to keep checking until nothing new is added
1086
                const dependentTypesChanged = new Set<string>();
1,496✔
1087
                let foundDependentTypes = false;
1,496✔
1088
                const changedTypeSymbols = changedSymbols.get(SymbolTypeFlag.typetime);
1,496✔
1089
                do {
1,496✔
1090
                    foundDependentTypes = false;
1,502✔
1091
                    const allChangedTypesSofar = [...Array.from(changedTypeSymbols), ...Array.from(dependentTypesChanged)];
1,502✔
1092
                    for (const changedSymbol of allChangedTypesSofar) {
1,502✔
1093
                        const symbolsDependentUponChangedSymbol = this.symbolDependencies.get(changedSymbol) ?? [];
1,055✔
1094
                        for (const symbolName of symbolsDependentUponChangedSymbol) {
1,055✔
1095
                            if (!changedTypeSymbols.has(symbolName) && !dependentTypesChanged.has(symbolName)) {
189✔
1096
                                foundDependentTypes = true;
6✔
1097
                                dependentTypesChanged.add(symbolName);
6✔
1098
                            }
1099
                        }
1100
                    }
1101
                } while (foundDependentTypes);
1102

1103
                changedSymbols.set(SymbolTypeFlag.typetime, new Set([...changedTypeSymbols, ...dependentTypesChanged]));
1,496✔
1104
            })
1105
            .once(() => {
1106
                if (this.options.logLevel === LogLevel.debug) {
1,496!
NEW
1107
                    const changedRuntime = Array.from(changedSymbols.get(SymbolTypeFlag.runtime)).sort();
×
NEW
1108
                    this.logger.debug('Changed Symbols (runTime):', changedRuntime.join(', '));
×
NEW
1109
                    const changedTypetime = Array.from(changedSymbols.get(SymbolTypeFlag.typetime)).sort();
×
NEW
1110
                    this.logger.debug('Changed Symbols (typeTime):', changedTypetime.join(', '));
×
1111
                }
1112
                filesToBeValidatedInScopeContext = new Set<BscFile>(filesToProcess);
1,496✔
1113

1114
                const scopesToCheck = this.getScopesForCrossScopeValidation(changedComponentTypes.length > 0);
1,496✔
1115
                this.crossScopeValidation.buildComponentsMap();
1,496✔
1116
                this.crossScopeValidation.addDiagnosticsForScopes(scopesToCheck);
1,496✔
1117
                const filesToRevalidate = this.crossScopeValidation.getFilesRequiringChangedSymbol(scopesToCheck, changedSymbols);
1,496✔
1118
                for (const file of filesToRevalidate) {
1,496✔
1119
                    filesToBeValidatedInScopeContext.add(file);
420✔
1120
                }
1121

1122
                this.currentScopeValidationOptions = {
1,496✔
1123
                    filesToBeValidatedInScopeContext: filesToBeValidatedInScopeContext,
1124
                    changedSymbols: changedSymbols,
1125
                    changedFiles: filesToProcess,
1126
                    initialValidation: this.isFirstValidation
1127
                };
1128
            })
1129
            .forEach(() => filesToBeValidatedInScopeContext, (file) => {
1,496✔
1130
                for (const file of filesToBeValidatedInScopeContext) {
2,303✔
1131
                    if (isBrsFile(file)) {
55,431✔
1132
                        file.validationSegmenter.unValidateAllSegments();
33,577✔
1133
                        for (const scope of this.getScopesForFile(file)) {
33,577✔
1134
                            scope.invalidate();
64,383✔
1135
                        }
1136
                    }
1137
                }
1138
            })
1139
            .forEach(() => this.getSortedScopeNames(), (scopeName) => {
1,496✔
1140
                //sort the scope names so we get consistent results
1141
                let scope = this.scopes[scopeName];
3,418✔
1142
                scope.validate(this.currentScopeValidationOptions);
3,418✔
1143
            })
1144
            .once(() => {
1145
                this.detectDuplicateComponentNames();
1,492✔
1146
                this.isFirstValidation = false;
1,492✔
1147
            })
1148
            .onCancel(() => {
1149
                logValidateEnd('cancelled');
10✔
1150
            })
1151
            .onSuccess(() => {
1152
                logValidateEnd();
1,492✔
1153
            })
1154
            .onComplete(() => {
1155
                //if we emitted the beforeProgramValidate hook, emit the afterProgramValidate hook as well
1156
                if (beforeProgramValidateWasEmitted) {
1,502✔
1157
                    const wasCancelled = options?.cancellationToken?.isCancellationRequested ?? false;
1,500✔
1158
                    this.plugins.emit('afterProgramValidate', {
1,500✔
1159
                        program: this,
1160
                        wasCancelled: wasCancelled
1161
                    });
1162
                }
1163

1164
                //regardless of the success of the validation, mark this run as complete
1165
                deferred.resolve();
1,502✔
1166
                //clear the validatePromise which means we're no longer running a validation
1167
                this.validatePromise = undefined;
1,502✔
1168
            });
1169

1170
        //run the sequencer in async mode if enabled
1171
        if (options?.async) {
1,502✔
1172
            return sequencer.run();
124✔
1173

1174
            //run the sequencer in sync mode
1175
        } else {
1176
            return sequencer.runSync();
1,378✔
1177
        }
1178
    }
1179

1180
    protected logValidationMetrics(metrics: Record<string, number | string>) {
UNCOV
1181
        let logs = [] as string[];
×
UNCOV
1182
        for (const key in metrics) {
×
UNCOV
1183
            logs.push(`${key}=${chalk.yellow(metrics[key].toString())}`);
×
1184
        }
UNCOV
1185
        this.logger.info(`Validation Metrics: ${logs.join(', ')}`);
×
1186
    }
1187

1188
    private getScopesForCrossScopeValidation(someComponentTypeChanged = false) {
×
1189
        const scopesForCrossScopeValidation = [];
1,496✔
1190
        for (let scopeName of this.getSortedScopeNames()) {
1,496✔
1191
            let scope = this.scopes[scopeName];
3,423✔
1192
            if (this.globalScope !== scope && (someComponentTypeChanged || !scope.isValidated)) {
3,423✔
1193
                scopesForCrossScopeValidation.push(scope);
1,884✔
1194
            }
1195
        }
1196
        return scopesForCrossScopeValidation;
1,496✔
1197
    }
1198

1199
    /**
1200
     * Flag all duplicate component names
1201
     */
1202
    private detectDuplicateComponentNames() {
1203
        const componentsByName = Object.keys(this.files).reduce<Record<string, XmlFile[]>>((map, filePath) => {
1,492✔
1204
            const file = this.files[filePath];
2,552✔
1205
            //if this is an XmlFile, and it has a valid `componentName` property
1206
            if (isXmlFile(file) && file.componentName?.text) {
2,552✔
1207
                let lowerName = file.componentName.text.toLowerCase();
556✔
1208
                if (!map[lowerName]) {
556✔
1209
                    map[lowerName] = [];
553✔
1210
                }
1211
                map[lowerName].push(file);
556✔
1212
            }
1213
            return map;
2,552✔
1214
        }, {});
1215

1216
        for (let name in componentsByName) {
1,492✔
1217
            const xmlFiles = componentsByName[name];
553✔
1218
            //add diagnostics for every duplicate component with this name
1219
            if (xmlFiles.length > 1) {
553✔
1220
                for (let xmlFile of xmlFiles) {
3✔
1221
                    const { componentName } = xmlFile;
6✔
1222
                    this.diagnostics.register({
6✔
1223
                        ...DiagnosticMessages.duplicateComponentName(componentName.text),
1224
                        location: xmlFile.componentName.location,
1225
                        relatedInformation: xmlFiles.filter(x => x !== xmlFile).map(x => {
12✔
1226
                            return {
6✔
1227
                                location: x.componentName.location,
1228
                                message: 'Also defined here'
1229
                            };
1230
                        })
1231
                    }, { tags: [ProgramValidatorDiagnosticsTag] });
1232
                }
1233
            }
1234
        }
1235
    }
1236

1237
    /**
1238
     * Get the files for a list of filePaths
1239
     * @param filePaths can be an array of srcPath or a destPath strings
1240
     * @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
1241
     */
1242
    public getFiles<T extends BscFile>(filePaths: string[], normalizePath = true) {
29✔
1243
        return filePaths
29✔
1244
            .map(filePath => this.getFile(filePath, normalizePath))
39✔
1245
            .filter(file => file !== undefined) as T[];
39✔
1246
    }
1247

1248
    private getFilePathCache = new Map<string, { path: string; isDestMap?: boolean }>();
1,936✔
1249

1250
    /**
1251
     * Get the file at the given path
1252
     * @param filePath can be a srcPath or a destPath
1253
     * @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
1254
     */
1255
    public getFile<T extends BscFile>(filePath: string, normalizePath = true) {
21,225✔
1256
        if (this.getFilePathCache.has(filePath)) {
271,285✔
1257
            const cachedFilePath = this.getFilePathCache.get(filePath);
260,355✔
1258
            if (cachedFilePath.isDestMap) {
260,355✔
1259
                return this.destMap.get(
257,750✔
1260
                    cachedFilePath.path
1261
                ) as T;
1262
            }
1263
            return this.files[
2,605✔
1264
                cachedFilePath.path
1265
            ] as T;
1266
        }
1267
        if (typeof filePath !== 'string') {
10,930✔
1268
            return undefined;
3,607✔
1269
            //is the path absolute (or the `virtual:` prefix)
1270
        } else if (/^(?:(?:virtual:[\/\\])|(?:\w:)|(?:[\/\\]))/gmi.exec(filePath)) {
7,323✔
1271
            const standardizedPath = (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase();
3,483!
1272
            this.getFilePathCache.set(filePath, { path: standardizedPath });
3,483✔
1273

1274
            return this.files[
3,483✔
1275
                standardizedPath
1276
            ] as T;
1277
        } else if (util.isUriLike(filePath)) {
3,840✔
1278
            const path = URI.parse(filePath).fsPath;
572✔
1279
            const standardizedPath = (normalizePath ? util.standardizePath(path) : path).toLowerCase();
572!
1280
            this.getFilePathCache.set(filePath, { path: standardizedPath });
572✔
1281

1282
            return this.files[
572✔
1283
                standardizedPath
1284
            ] as T;
1285
        } else {
1286
            const standardizedPath = (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase();
3,268✔
1287
            this.getFilePathCache.set(filePath, { path: standardizedPath, isDestMap: true });
3,268✔
1288
            return this.destMap.get(
3,268✔
1289
                standardizedPath
1290
            ) as T;
1291
        }
1292
    }
1293

1294
    private sortedScopeNames: string[] = undefined;
1,936✔
1295

1296
    /**
1297
     * Gets a sorted list of all scopeNames, always beginning with "global", "source", then any others in alphabetical order
1298
     */
1299
    private getSortedScopeNames() {
1300
        if (!this.sortedScopeNames) {
43,332✔
1301
            this.sortedScopeNames = Object.keys(this.scopes).sort((a, b) => {
1,424✔
1302
                if (a === 'global') {
2,070!
UNCOV
1303
                    return -1;
×
1304
                } else if (b === 'global') {
2,070✔
1305
                    return 1;
1,386✔
1306
                }
1307
                if (a === 'source') {
684✔
1308
                    return -1;
28✔
1309
                } else if (b === 'source') {
656✔
1310
                    return 1;
150✔
1311
                }
1312
                if (a < b) {
506✔
1313
                    return -1;
198✔
1314
                } else if (b < a) {
308!
1315
                    return 1;
308✔
1316
                }
UNCOV
1317
                return 0;
×
1318
            });
1319
        }
1320
        return this.sortedScopeNames;
43,332✔
1321
    }
1322

1323
    /**
1324
     * Get a list of all scopes the file is loaded into
1325
     * @param file the file
1326
     */
1327
    public getScopesForFile(file: BscFile | string) {
1328
        const resolvedFile = typeof file === 'string' ? this.getFile(file) : file;
36,128✔
1329

1330
        let result = [] as Scope[];
36,128✔
1331
        if (resolvedFile) {
36,128✔
1332
            const scopeKeys = this.getSortedScopeNames();
36,127✔
1333
            for (let key of scopeKeys) {
36,127✔
1334
                let scope = this.scopes[key];
3,135,919✔
1335

1336
                if (scope.hasFile(resolvedFile)) {
3,135,919✔
1337
                    result.push(scope);
67,229✔
1338
                }
1339
            }
1340
        }
1341
        return result;
36,128✔
1342
    }
1343

1344
    /**
1345
     * Get the first found scope for a file.
1346
     */
1347
    public getFirstScopeForFile(file: BscFile): Scope | undefined {
1348
        const scopeKeys = this.getSortedScopeNames();
4,213✔
1349
        for (let key of scopeKeys) {
4,213✔
1350
            let scope = this.scopes[key];
18,880✔
1351

1352
            if (scope.hasFile(file)) {
18,880✔
1353
                return scope;
3,069✔
1354
            }
1355
        }
1356
    }
1357

1358
    public getStatementsByName(name: string, originFile: BrsFile, namespaceName?: string): FileLink<Statement>[] {
1359
        let results = new Map<Statement, FileLink<Statement>>();
39✔
1360
        const filesSearched = new Set<BrsFile>();
39✔
1361
        let lowerNamespaceName = namespaceName?.toLowerCase();
39✔
1362
        let lowerName = name?.toLowerCase();
39!
1363

1364
        function addToResults(statement: FunctionStatement | MethodStatement, file: BrsFile) {
1365
            let parentNamespaceName = statement.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(originFile.parseMode)?.toLowerCase();
98✔
1366
            if (statement.tokens.name.text.toLowerCase() === lowerName && (!lowerNamespaceName || parentNamespaceName === lowerNamespaceName)) {
98✔
1367
                if (!results.has(statement)) {
36!
1368
                    results.set(statement, { item: statement, file: file as BrsFile });
36✔
1369
                }
1370
            }
1371
        }
1372

1373
        //look through all files in scope for matches
1374
        for (const scope of this.getScopesForFile(originFile)) {
39✔
1375
            for (const file of scope.getAllFiles()) {
39✔
1376
                //skip non-brs files, or files we've already processed
1377
                if (!isBrsFile(file) || filesSearched.has(file)) {
45✔
1378
                    continue;
3✔
1379
                }
1380
                filesSearched.add(file);
42✔
1381

1382
                file.ast.walk(createVisitor({
42✔
1383
                    FunctionStatement: (statement: FunctionStatement) => {
1384
                        addToResults(statement, file);
95✔
1385
                    },
1386
                    MethodStatement: (statement: MethodStatement) => {
1387
                        addToResults(statement, file);
3✔
1388
                    }
1389
                }), {
1390
                    walkMode: WalkMode.visitStatements
1391
                });
1392
            }
1393
        }
1394
        return [...results.values()];
39✔
1395
    }
1396

1397
    public getStatementsForXmlFile(scope: XmlScope, filterName?: string): FileLink<FunctionStatement>[] {
1398
        let results = new Map<Statement, FileLink<FunctionStatement>>();
14✔
1399
        const filesSearched = new Set<BrsFile>();
14✔
1400

1401
        //get all function names for the xml file and parents
1402
        let funcNames = new Set<string>();
14✔
1403
        let currentScope = scope;
14✔
1404
        while (isXmlScope(currentScope)) {
14✔
1405
            for (let name of currentScope.xmlFile.ast.componentElement.interfaceElement?.functions.map((f) => f.name) ?? []) {
20✔
1406
                if (!filterName || name === filterName) {
20!
1407
                    funcNames.add(name);
20✔
1408
                }
1409
            }
1410
            currentScope = currentScope.getParentScope() as XmlScope;
16✔
1411
        }
1412

1413
        //look through all files in scope for matches
1414
        for (const file of scope.getOwnFiles()) {
14✔
1415
            //skip non-brs files, or files we've already processed
1416
            if (!isBrsFile(file) || filesSearched.has(file)) {
28✔
1417
                continue;
14✔
1418
            }
1419
            filesSearched.add(file);
14✔
1420

1421
            file.ast.walk(createVisitor({
14✔
1422
                FunctionStatement: (statement: FunctionStatement) => {
1423
                    if (funcNames.has(statement.tokens.name.text)) {
19!
1424
                        if (!results.has(statement)) {
19!
1425
                            results.set(statement, { item: statement, file: file });
19✔
1426
                        }
1427
                    }
1428
                }
1429
            }), {
1430
                walkMode: WalkMode.visitStatements
1431
            });
1432
        }
1433
        return [...results.values()];
14✔
1434
    }
1435

1436
    /**
1437
     * Find all available completion items at the given position
1438
     * @param filePath can be a srcPath or a destPath
1439
     * @param position the position (line & column) where completions should be found
1440
     */
1441
    public getCompletions(filePath: string, position: Position) {
1442
        let file = this.getFile(filePath);
123✔
1443
        if (!file) {
123!
UNCOV
1444
            return [];
×
1445
        }
1446

1447
        //find the scopes for this file
1448
        let scopes = this.getScopesForFile(file);
123✔
1449

1450
        //if there are no scopes, include the global scope so we at least get the built-in functions
1451
        scopes = scopes.length > 0 ? scopes : [this.globalScope];
123!
1452

1453
        const event: ProvideCompletionsEvent = {
123✔
1454
            program: this,
1455
            file: file,
1456
            scopes: scopes,
1457
            position: position,
1458
            completions: []
1459
        };
1460

1461
        this.plugins.emit('beforeProvideCompletions', event);
123✔
1462

1463
        this.plugins.emit('provideCompletions', event);
123✔
1464

1465
        this.plugins.emit('afterProvideCompletions', event);
123✔
1466

1467
        return event.completions;
123✔
1468
    }
1469

1470
    /**
1471
     * Goes through each file and builds a list of workspace symbols for the program. Used by LanguageServer's onWorkspaceSymbol functionality
1472
     */
1473
    public getWorkspaceSymbols() {
1474
        const event: ProvideWorkspaceSymbolsEvent = {
22✔
1475
            program: this,
1476
            workspaceSymbols: []
1477
        };
1478
        this.plugins.emit('beforeProvideWorkspaceSymbols', event);
22✔
1479
        this.plugins.emit('provideWorkspaceSymbols', event);
22✔
1480
        this.plugins.emit('afterProvideWorkspaceSymbols', event);
22✔
1481
        return event.workspaceSymbols;
22✔
1482
    }
1483

1484
    /**
1485
     * Given a position in a file, if the position is sitting on some type of identifier,
1486
     * go to the definition of that identifier (where this thing was first defined)
1487
     */
1488
    public getDefinition(srcPath: string, position: Position): Location[] {
1489
        let file = this.getFile(srcPath);
18✔
1490
        if (!file) {
18!
UNCOV
1491
            return [];
×
1492
        }
1493

1494
        const event: ProvideDefinitionEvent = {
18✔
1495
            program: this,
1496
            file: file,
1497
            position: position,
1498
            definitions: []
1499
        };
1500

1501
        this.plugins.emit('beforeProvideDefinition', event);
18✔
1502
        this.plugins.emit('provideDefinition', event);
18✔
1503
        this.plugins.emit('afterProvideDefinition', event);
18✔
1504
        return event.definitions;
18✔
1505
    }
1506

1507
    /**
1508
     * Get hover information for a file and position
1509
     */
1510
    public getHover(srcPath: string, position: Position): Hover[] {
1511
        let file = this.getFile(srcPath);
69✔
1512
        let result: Hover[];
1513
        if (file) {
69!
1514
            const event = {
69✔
1515
                program: this,
1516
                file: file,
1517
                position: position,
1518
                scopes: this.getScopesForFile(file),
1519
                hovers: []
1520
            } as ProvideHoverEvent;
1521
            this.plugins.emit('beforeProvideHover', event);
69✔
1522
            this.plugins.emit('provideHover', event);
69✔
1523
            this.plugins.emit('afterProvideHover', event);
69✔
1524
            result = event.hovers;
69✔
1525
        }
1526

1527
        return result ?? [];
69!
1528
    }
1529

1530
    /**
1531
     * Get full list of document symbols for a file
1532
     * @param srcPath path to the file
1533
     */
1534
    public getDocumentSymbols(srcPath: string): DocumentSymbol[] | undefined {
1535
        let file = this.getFile(srcPath);
24✔
1536
        if (file) {
24!
1537
            const event: ProvideDocumentSymbolsEvent = {
24✔
1538
                program: this,
1539
                file: file,
1540
                documentSymbols: []
1541
            };
1542
            this.plugins.emit('beforeProvideDocumentSymbols', event);
24✔
1543
            this.plugins.emit('provideDocumentSymbols', event);
24✔
1544
            this.plugins.emit('afterProvideDocumentSymbols', event);
24✔
1545
            return event.documentSymbols;
24✔
1546
        } else {
UNCOV
1547
            return undefined;
×
1548
        }
1549
    }
1550

1551
    /**
1552
     * Compute code actions for the given file and range
1553
     */
1554
    public getCodeActions(srcPath: string, range: Range) {
1555
        const codeActions = [] as CodeAction[];
12✔
1556
        const file = this.getFile(srcPath);
12✔
1557
        if (file) {
12✔
1558
            const fileUri = util.pathToUri(file?.srcPath);
11!
1559
            const diagnostics = this
11✔
1560
                //get all current diagnostics (filtered by diagnostic filters)
1561
                .getDiagnostics()
1562
                //only keep diagnostics related to this file
1563
                .filter(x => x.location?.uri === fileUri)
21!
1564
                //only keep diagnostics that touch this range
1565
                .filter(x => util.rangesIntersectOrTouch(x.location.range, range));
12✔
1566

1567
            const scopes = this.getScopesForFile(file);
11✔
1568

1569
            this.plugins.emit('onGetCodeActions', {
11✔
1570
                program: this,
1571
                file: file,
1572
                range: range,
1573
                diagnostics: diagnostics,
1574
                scopes: scopes,
1575
                codeActions: codeActions
1576
            });
1577
        }
1578
        return codeActions;
12✔
1579
    }
1580

1581
    /**
1582
     * Get semantic tokens for the specified file
1583
     */
1584
    public getSemanticTokens(srcPath: string): SemanticToken[] | undefined {
1585
        const file = this.getFile(srcPath);
24✔
1586
        if (file) {
24!
1587
            const result = [] as SemanticToken[];
24✔
1588
            this.plugins.emit('onGetSemanticTokens', {
24✔
1589
                program: this,
1590
                file: file,
1591
                scopes: this.getScopesForFile(file),
1592
                semanticTokens: result
1593
            });
1594
            return result;
24✔
1595
        }
1596
    }
1597

1598
    public getSignatureHelp(filepath: string, position: Position): SignatureInfoObj[] {
1599
        let file: BrsFile = this.getFile(filepath);
185✔
1600
        if (!file || !isBrsFile(file)) {
185✔
1601
            return [];
3✔
1602
        }
1603
        let callExpressionInfo = new CallExpressionInfo(file, position);
182✔
1604
        let signatureHelpUtil = new SignatureHelpUtil();
182✔
1605
        return signatureHelpUtil.getSignatureHelpItems(callExpressionInfo);
182✔
1606
    }
1607

1608
    public getReferences(srcPath: string, position: Position): Location[] {
1609
        //find the file
1610
        let file = this.getFile(srcPath);
4✔
1611

1612
        const event: ProvideReferencesEvent = {
4✔
1613
            program: this,
1614
            file: file,
1615
            position: position,
1616
            references: []
1617
        };
1618

1619
        this.plugins.emit('beforeProvideReferences', event);
4✔
1620
        this.plugins.emit('provideReferences', event);
4✔
1621
        this.plugins.emit('afterProvideReferences', event);
4✔
1622

1623
        return event.references;
4✔
1624
    }
1625

1626
    /**
1627
     * Transpile a single file and get the result as a string.
1628
     * This does not write anything to the file system.
1629
     *
1630
     * This should only be called by `LanguageServer`.
1631
     * Internal usage should call `_getTranspiledFileContents` instead.
1632
     * @param filePath can be a srcPath or a destPath
1633
     */
1634
    public async getTranspiledFileContents(filePath: string): Promise<FileTranspileResult> {
1635
        const file = this.getFile(filePath);
319✔
1636

1637
        return this.getTranspiledFileContentsPipeline.run(async () => {
319✔
1638

1639
            const result = {
319✔
1640
                destPath: file.destPath,
1641
                pkgPath: file.pkgPath,
1642
                srcPath: file.srcPath
1643
            } as FileTranspileResult;
1644

1645
            const expectedPkgPath = file.pkgPath.toLowerCase();
319✔
1646
            const expectedMapPath = `${expectedPkgPath}.map`;
319✔
1647
            const expectedTypedefPkgPath = expectedPkgPath.replace(/\.brs$/i, '.d.bs');
319✔
1648

1649
            //add a temporary plugin to tap into the file writing process
1650
            const plugin = this.plugins.addFirst({
319✔
1651
                name: 'getTranspiledFileContents',
1652
                beforeWriteFile: (event) => {
1653
                    const pkgPath = event.file.pkgPath.toLowerCase();
994✔
1654
                    switch (pkgPath) {
994✔
1655
                        //this is the actual transpiled file
1656
                        case expectedPkgPath:
994✔
1657
                            result.code = event.file.data.toString();
319✔
1658
                            break;
319✔
1659
                        //this is the sourcemap
1660
                        case expectedMapPath:
1661
                            result.map = event.file.data.toString();
170✔
1662
                            break;
170✔
1663
                        //this is the typedef
1664
                        case expectedTypedefPkgPath:
1665
                            result.typedef = event.file.data.toString();
8✔
1666
                            break;
8✔
1667
                        default:
1668
                        //no idea what this file is. just ignore it
1669
                    }
1670
                    //mark every file as processed so it they don't get written to the output directory
1671
                    event.processedFiles.add(event.file);
994✔
1672
                }
1673
            });
1674

1675
            try {
319✔
1676
                //now that the plugin has been registered, run the build with just this file
1677
                await this.build({
319✔
1678
                    files: [file]
1679
                });
1680
            } finally {
1681
                this.plugins.remove(plugin);
319✔
1682
            }
1683
            return result;
319✔
1684
        });
1685
    }
1686
    private getTranspiledFileContentsPipeline = new ActionPipeline();
1,936✔
1687

1688
    /**
1689
     * Get the absolute output path for a file
1690
     */
1691
    private getOutputPath(file: { pkgPath?: string }, stagingDir = this.getStagingDir()) {
×
1692
        return s`${stagingDir}/${file.pkgPath}`;
1,835✔
1693
    }
1694

1695
    private getStagingDir(stagingDir?: string) {
1696
        let result = stagingDir ?? this.options.stagingDir ?? this.options.stagingDir;
719✔
1697
        if (!result) {
719✔
1698
            result = rokuDeploy.getOptions(this.options as any).stagingDir;
531✔
1699
        }
1700
        result = s`${path.resolve(this.options.cwd ?? process.cwd(), result ?? '/')}`;
719!
1701
        return result;
719✔
1702
    }
1703

1704
    /**
1705
     * Prepare the program for building
1706
     * @param files the list of files that should be prepared
1707
     */
1708
    private async prepare(files: BscFile[]) {
1709
        const programEvent: PrepareProgramEvent = {
360✔
1710
            program: this,
1711
            editor: this.editor,
1712
            files: files
1713
        };
1714

1715
        //assign an editor to every file
1716
        for (const file of programEvent.files) {
360✔
1717
            //if the file doesn't have an editor yet, assign one now
1718
            if (!file.editor) {
730✔
1719
                file.editor = new Editor();
683✔
1720
            }
1721
        }
1722

1723
        //sort the entries to make transpiling more deterministic
1724
        programEvent.files.sort((a, b) => {
360✔
1725
            if (a.pkgPath < b.pkgPath) {
386✔
1726
                return -1;
325✔
1727
            } else if (a.pkgPath > b.pkgPath) {
61!
1728
                return 1;
61✔
1729
            } else {
UNCOV
1730
                return 1;
×
1731
            }
1732
        });
1733

1734
        await this.plugins.emitAsync('beforePrepareProgram', programEvent);
360✔
1735
        await this.plugins.emitAsync('prepareProgram', programEvent);
360✔
1736

1737
        const stagingDir = this.getStagingDir();
360✔
1738

1739
        const entries: TranspileObj[] = [];
360✔
1740

1741
        for (const file of files) {
360✔
1742
            const scope = this.getFirstScopeForFile(file);
730✔
1743
            //link the symbol table for all the files in this scope
1744
            scope?.linkSymbolTable();
730✔
1745

1746
            //if the file doesn't have an editor yet, assign one now
1747
            if (!file.editor) {
730!
UNCOV
1748
                file.editor = new Editor();
×
1749
            }
1750
            const event = {
730✔
1751
                program: this,
1752
                file: file,
1753
                editor: file.editor,
1754
                scope: scope,
1755
                outputPath: this.getOutputPath(file, stagingDir)
1756
            } as PrepareFileEvent & { outputPath: string };
1757

1758
            await this.plugins.emitAsync('beforePrepareFile', event);
730✔
1759
            await this.plugins.emitAsync('prepareFile', event);
730✔
1760
            await this.plugins.emitAsync('afterPrepareFile', event);
730✔
1761

1762
            //TODO remove this in v1
1763
            entries.push(event);
730✔
1764

1765
            //unlink the symbolTable so the next loop iteration can link theirs
1766
            scope?.unlinkSymbolTable();
730✔
1767
        }
1768

1769
        await this.plugins.emitAsync('afterPrepareProgram', programEvent);
360✔
1770
        return files;
360✔
1771
    }
1772

1773
    /**
1774
     * Generate the contents of every file
1775
     */
1776
    private async serialize(files: BscFile[]) {
1777

1778
        const allFiles = new Map<BscFile, SerializedFile[]>();
359✔
1779

1780
        //exclude prunable files if that option is enabled
1781
        if (this.options.pruneEmptyCodeFiles === true) {
359✔
1782
            files = files.filter(x => x.canBePruned !== true);
9✔
1783
        }
1784

1785
        const serializeProgramEvent = await this.plugins.emitAsync('beforeSerializeProgram', {
359✔
1786
            program: this,
1787
            files: files,
1788
            result: allFiles
1789
        });
1790
        await this.plugins.emitAsync('onSerializeProgram', serializeProgramEvent);
359✔
1791

1792
        // serialize each file
1793
        for (const file of files) {
359✔
1794
            let scope = this.getFirstScopeForFile(file);
727✔
1795

1796
            //if the file doesn't have a scope, create a temporary scope for the file so it can depend on scope-level items
1797
            if (!scope) {
727✔
1798
                scope = new Scope(`temporary-for-${file.pkgPath}`, this);
370✔
1799
                scope.getAllFiles = () => [file];
3,317✔
1800
                scope.getOwnFiles = scope.getAllFiles;
370✔
1801
            }
1802

1803
            //link the symbol table for all the files in this scope
1804
            scope?.linkSymbolTable();
727!
1805
            const event: SerializeFileEvent = {
727✔
1806
                program: this,
1807
                file: file,
1808
                scope: scope,
1809
                result: allFiles
1810
            };
1811
            await this.plugins.emitAsync('beforeSerializeFile', event);
727✔
1812
            await this.plugins.emitAsync('serializeFile', event);
727✔
1813
            await this.plugins.emitAsync('afterSerializeFile', event);
727✔
1814
            //unlink the symbolTable so the next loop iteration can link theirs
1815
            scope?.unlinkSymbolTable();
727!
1816
        }
1817

1818
        this.plugins.emit('afterSerializeProgram', serializeProgramEvent);
359✔
1819

1820
        return allFiles;
359✔
1821
    }
1822

1823
    /**
1824
     * Write the entire project to disk
1825
     */
1826
    private async write(stagingDir: string, files: Map<BscFile, SerializedFile[]>) {
1827
        const programEvent = await this.plugins.emitAsync('beforeWriteProgram', {
359✔
1828
            program: this,
1829
            files: files,
1830
            stagingDir: stagingDir
1831
        });
1832
        //empty the staging directory
1833
        await fsExtra.emptyDir(stagingDir);
359✔
1834

1835
        const serializedFiles = [...files]
359✔
1836
            .map(([, serializedFiles]) => serializedFiles)
727✔
1837
            .flat();
1838

1839
        //write all the files to disk (asynchronously)
1840
        await Promise.all(
359✔
1841
            serializedFiles.map(async (file) => {
1842
                const event = await this.plugins.emitAsync('beforeWriteFile', {
1,105✔
1843
                    program: this,
1844
                    file: file,
1845
                    outputPath: this.getOutputPath(file, stagingDir),
1846
                    processedFiles: new Set<SerializedFile>()
1847
                });
1848

1849
                await this.plugins.emitAsync('writeFile', event);
1,105✔
1850

1851
                await this.plugins.emitAsync('afterWriteFile', event);
1,105✔
1852
            })
1853
        );
1854

1855
        await this.plugins.emitAsync('afterWriteProgram', programEvent);
359✔
1856
    }
1857

1858
    private buildPipeline = new ActionPipeline();
1,936✔
1859

1860
    /**
1861
     * Build the project. This transpiles/transforms/copies all files and moves them to the staging directory
1862
     * @param options the list of options used to build the program
1863
     */
1864
    public async build(options?: ProgramBuildOptions) {
1865
        //run a single build at a time
1866
        await this.buildPipeline.run(async () => {
359✔
1867
            const stagingDir = this.getStagingDir(options?.stagingDir);
359✔
1868

1869
            const event = await this.plugins.emitAsync('beforeBuildProgram', {
359✔
1870
                program: this,
1871
                editor: this.editor,
1872
                files: options?.files ?? Object.values(this.files)
2,154✔
1873
            });
1874

1875
            //prepare the program (and files) for building
1876
            event.files = await this.prepare(event.files);
359✔
1877

1878
            //stage the entire program
1879
            const serializedFilesByFile = await this.serialize(event.files);
359✔
1880

1881
            await this.write(stagingDir, serializedFilesByFile);
359✔
1882

1883
            await this.plugins.emitAsync('afterBuildProgram', event);
359✔
1884

1885
            //undo all edits for the program
1886
            this.editor.undoAll();
359✔
1887
            //undo all edits for each file
1888
            for (const file of event.files) {
359✔
1889
                file.editor.undoAll();
728✔
1890
            }
1891
        });
1892

1893
        console.log('TYPES CREATED', TypesCreated);
359✔
1894
        let totalTypesCreated = 0;
359✔
1895
        for (const key in TypesCreated) {
359✔
1896
            if (TypesCreated.hasOwnProperty(key)) {
9,326!
1897
                totalTypesCreated += TypesCreated[key];
9,326✔
1898

1899
            }
1900
        }
1901
        console.log('TOTAL TYPES CREATED', totalTypesCreated);
359✔
1902
    }
1903

1904
    /**
1905
     * Find a list of files in the program that have a function with the given name (case INsensitive)
1906
     */
1907
    public findFilesForFunction(functionName: string) {
1908
        const files = [] as BscFile[];
7✔
1909
        const lowerFunctionName = functionName.toLowerCase();
7✔
1910
        //find every file with this function defined
1911
        for (const file of Object.values(this.files)) {
7✔
1912
            if (isBrsFile(file)) {
25✔
1913
                //TODO handle namespace-relative function calls
1914
                //if the file has a function with this name
1915
                // eslint-disable-next-line @typescript-eslint/dot-notation
1916
                if (file['_cachedLookups'].functionStatementMap.get(lowerFunctionName)) {
17✔
1917
                    files.push(file);
2✔
1918
                }
1919
            }
1920
        }
1921
        return files;
7✔
1922
    }
1923

1924
    /**
1925
     * Find a list of files in the program that have a class with the given name (case INsensitive)
1926
     */
1927
    public findFilesForClass(className: string) {
1928
        const files = [] as BscFile[];
7✔
1929
        const lowerClassName = className.toLowerCase();
7✔
1930
        //find every file with this class defined
1931
        for (const file of Object.values(this.files)) {
7✔
1932
            if (isBrsFile(file)) {
25✔
1933
                //TODO handle namespace-relative classes
1934
                //if the file has a function with this name
1935

1936
                // eslint-disable-next-line @typescript-eslint/dot-notation
1937
                if (file['_cachedLookups'].classStatementMap.get(lowerClassName) !== undefined) {
17✔
1938
                    files.push(file);
1✔
1939
                }
1940
            }
1941
        }
1942
        return files;
7✔
1943
    }
1944

1945
    public findFilesForNamespace(name: string) {
1946
        const files = [] as BscFile[];
7✔
1947
        const lowerName = name.toLowerCase();
7✔
1948
        //find every file with this class defined
1949
        for (const file of Object.values(this.files)) {
7✔
1950
            if (isBrsFile(file)) {
25✔
1951

1952
                // eslint-disable-next-line @typescript-eslint/dot-notation
1953
                if (file['_cachedLookups'].namespaceStatements.find((x) => {
17✔
1954
                    const namespaceName = x.name.toLowerCase();
7✔
1955
                    return (
7✔
1956
                        //the namespace name matches exactly
1957
                        namespaceName === lowerName ||
9✔
1958
                        //the full namespace starts with the name (honoring the part boundary)
1959
                        namespaceName.startsWith(lowerName + '.')
1960
                    );
1961
                })) {
1962
                    files.push(file);
6✔
1963
                }
1964
            }
1965
        }
1966

1967
        return files;
7✔
1968
    }
1969

1970
    public findFilesForEnum(name: string) {
1971
        const files = [] as BscFile[];
8✔
1972
        const lowerName = name.toLowerCase();
8✔
1973
        //find every file with this enum defined
1974
        for (const file of Object.values(this.files)) {
8✔
1975
            if (isBrsFile(file)) {
26✔
1976
                // eslint-disable-next-line @typescript-eslint/dot-notation
1977
                if (file['_cachedLookups'].enumStatementMap.get(lowerName)) {
18✔
1978
                    files.push(file);
1✔
1979
                }
1980
            }
1981
        }
1982
        return files;
8✔
1983
    }
1984

1985
    private _manifest: Map<string, string>;
1986

1987
    /**
1988
     * Modify a parsed manifest map by reading `bs_const` and injecting values from `options.manifest.bs_const`
1989
     * @param parsedManifest The manifest map to read from and modify
1990
     */
1991
    private buildBsConstsIntoParsedManifest(parsedManifest: Map<string, string>) {
1992
        // Lift the bs_consts defined in the manifest
1993
        let bsConsts = getBsConst(parsedManifest, false);
17✔
1994

1995
        // Override or delete any bs_consts defined in the bs config
1996
        for (const key in this.options?.manifest?.bs_const) {
17!
1997
            const value = this.options.manifest.bs_const[key];
3✔
1998
            if (value === null) {
3✔
1999
                bsConsts.delete(key);
1✔
2000
            } else {
2001
                bsConsts.set(key, value);
2✔
2002
            }
2003
        }
2004

2005
        // convert the new list of bs consts back into a string for the rest of the down stream systems to use
2006
        let constString = '';
17✔
2007
        for (const [key, value] of bsConsts) {
17✔
2008
            constString += `${constString !== '' ? ';' : ''}${key}=${value.toString()}`;
8✔
2009
        }
2010

2011
        // Set the updated bs_const value
2012
        parsedManifest.set('bs_const', constString);
17✔
2013
    }
2014

2015
    /**
2016
     * Try to find and load the manifest into memory
2017
     * @param manifestFileObj A pointer to a potential manifest file object found during loading
2018
     * @param replaceIfAlreadyLoaded should we overwrite the internal `_manifest` if it already exists
2019
     */
2020
    public loadManifest(manifestFileObj?: FileObj, replaceIfAlreadyLoaded = true) {
1,571✔
2021
        //if we already have a manifest instance, and should not replace...then don't replace
2022
        if (!replaceIfAlreadyLoaded && this._manifest) {
1,579!
UNCOV
2023
            return;
×
2024
        }
2025
        let manifestPath = manifestFileObj
1,579✔
2026
            ? manifestFileObj.src
1,579✔
2027
            : path.join(this.options.rootDir, 'manifest');
2028

2029
        try {
1,579✔
2030
            // we only load this manifest once, so do it sync to improve speed downstream
2031
            const contents = fsExtra.readFileSync(manifestPath, 'utf-8');
1,579✔
2032
            const parsedManifest = parseManifest(contents);
17✔
2033
            this.buildBsConstsIntoParsedManifest(parsedManifest);
17✔
2034
            this._manifest = parsedManifest;
17✔
2035
        } catch (e) {
2036
            this._manifest = new Map();
1,562✔
2037
        }
2038
    }
2039

2040
    /**
2041
     * Get a map of the manifest information
2042
     */
2043
    public getManifest() {
2044
        if (!this._manifest) {
2,518✔
2045
            this.loadManifest();
1,570✔
2046
        }
2047
        return this._manifest;
2,518✔
2048
    }
2049

2050
    public dispose() {
2051
        this.plugins.emit('beforeProgramDispose', { program: this });
1,810✔
2052

2053
        for (let filePath in this.files) {
1,810✔
2054
            this.files[filePath]?.dispose?.();
2,295!
2055
        }
2056
        for (let name in this.scopes) {
1,810✔
2057
            this.scopes[name]?.dispose?.();
3,813!
2058
        }
2059
        this.globalScope?.dispose?.();
1,810!
2060
        this.dependencyGraph?.dispose?.();
1,810!
2061
    }
2062
}
2063

2064
export interface FileTranspileResult {
2065
    srcPath: string;
2066
    destPath: string;
2067
    pkgPath: string;
2068
    code: string;
2069
    map: string;
2070
    typedef: string;
2071
}
2072

2073

2074
class ProvideFileEventInternal<TFile extends BscFile = BscFile> implements ProvideFileEvent<TFile> {
2075
    constructor(
2076
        public program: Program,
2,603✔
2077
        public srcPath: string,
2,603✔
2078
        public destPath: string,
2,603✔
2079
        public data: LazyFileData,
2,603✔
2080
        public fileFactory: FileFactory
2,603✔
2081
    ) {
2082
        this.srcExtension = path.extname(srcPath)?.toLowerCase();
2,603!
2083
    }
2084

2085
    public srcExtension: string;
2086

2087
    public files: TFile[] = [];
2,603✔
2088
}
2089

2090
export interface ProgramBuildOptions {
2091
    /**
2092
     * The directory where the final built files should be placed. This directory will be cleared before running
2093
     */
2094
    stagingDir?: string;
2095
    /**
2096
     * An array of files to build. If omitted, the entire list of files from the program will be used instead.
2097
     * Typically you will want to leave this blank
2098
     */
2099
    files?: BscFile[];
2100
}
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