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

rokucommunity / brighterscript / 29442273139

15 Jul 2026 06:51PM UTC coverage: 86.919%. First build
29442273139

Pull #1750

github

web-flow
Merge 243030cb9 into e606c42ad
Pull Request #1750: Add `compilerOptions` group to bsconfig.json

16185 of 19649 branches covered (82.37%)

Branch coverage included in aggregate %.

31 of 32 new or added lines in 5 files covered. (96.88%)

16899 of 18414 relevant lines covered (91.77%)

27750.31 hits per line

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

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

71
const bslibNonAliasedRokuModulesPkgPath = s`source/roku_modules/rokucommunity_bslib/bslib.brs`;
1✔
72
const bslibAliasedRokuModulesPkgPath = s`source/roku_modules/bslib/bslib.brs`;
1✔
73

74
export interface SignatureInfoObj {
75
    index: number;
76
    key: string;
77
    signature: SignatureInformation;
78
}
79

80
export class Program {
1✔
81
    constructor(
82
        /**
83
         * The root directory for this program
84
         */
85
        options: BsConfig,
86
        logger?: Logger,
87
        plugins?: PluginInterface,
88
        diagnosticsManager?: DiagnosticManager
89
    ) {
90
        this.options = util.normalizeConfig(options);
2,663✔
91
        this.logger = logger ?? createLogger(options);
2,663✔
92
        this.plugins = plugins || new PluginInterface([], { logger: this.logger });
2,663✔
93
        this.diagnostics = diagnosticsManager || new DiagnosticManager();
2,663✔
94

95
        //surface warnings for any deprecated bsconfig options that were used to build `this.options`
96
        const deprecationDiagnostics = (this.options as any)._deprecationDiagnostics as BsDiagnostic[];
2,663✔
97
        if (deprecationDiagnostics?.length > 0) {
2,663!
NEW
98
            this.diagnostics.register(deprecationDiagnostics);
×
99
        }
100
        delete (this.options as any)._deprecationDiagnostics;
2,663✔
101

102
        //try to find a location for the diagnostic if it doesn't have one
103
        this.diagnostics.locationResolver = (args) => {
2,663✔
104

105
            //find the first xml scope for this diagnostic
106
            for (let context of args.contexts) {
6✔
107
                if (isXmlScope(context.scope) && isXmlFile(context.scope.xmlFile)) {
1!
108
                    return util.createLocation(0, 0, 0, 100, context.scope.xmlFile.srcPath);
1✔
109
                }
110
            }
111

112
            //we couldn't find an xml scope for this, so try to find the manifest file instead
113
            const manifest = this.getFile('manifest', false);
5✔
114
            if (manifest) {
5✔
115
                return util.createLocation(0, 0, 0, 100, manifest.srcPath);
3✔
116
            }
117

118
            //if we still don't have a manifest, try to find the first file in the program
119
            for (const key in this.files) {
2✔
120
                if (isBrsFile(this.files[key]) || isXmlFile(this.files[key])) {
2!
121
                    return util.createLocation(0, 0, 0, 100, this.files[key].srcPath);
2✔
122
                }
123
            }
124

125
            this.logger.warn(`Unable to find a location for the diagnostic.`, args);
×
126

127
            //we couldn't find any locations for the file, so just return undefined
128
            return undefined;
×
129
        };
130

131
        // initialize the diagnostics Manager
132
        this.diagnostics.logger = this.logger;
2,663✔
133
        this.diagnostics.options = this.options;
2,663✔
134
        this.diagnostics.program = this;
2,663✔
135

136
        //inject the bsc plugin as the first plugin in the stack.
137
        this.plugins.addFirst(new BscPlugin());
2,663✔
138

139
        //normalize the root dir path
140
        this.options.rootDir = util.getRootDir(this.options);
2,663✔
141

142
        this.createGlobalScope();
2,663✔
143

144
        this.fileFactory = new FileFactory(this);
2,663✔
145
    }
146

147
    public options: FinalizedBsConfig;
148
    public logger: Logger;
149

150
    /**
151
     * 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`)
152
     */
153
    public editor = new Editor();
2,663✔
154

155
    /**
156
     * A factory that creates `File` instances
157
     */
158
    private fileFactory: FileFactory;
159

160
    private createGlobalScope() {
161
        //create the 'global' scope
162
        this.globalScope = new Scope('global', this, 'scope:global');
2,663✔
163
        this.globalScope.attachDependencyGraph(this.dependencyGraph);
2,663✔
164
        this.scopes.global = this.globalScope;
2,663✔
165

166
        this.populateGlobalSymbolTable();
2,663✔
167
        this.globalScope.symbolTable.addSibling(this.componentsTable);
2,663✔
168

169
        //hardcode the files list for global scope to only contain the global file
170
        this.globalScope.getAllFiles = () => [globalFile];
16,773✔
171
        globalFile.isValidated = true;
2,663✔
172
        this.globalScope.validate();
2,663✔
173

174
        //TODO we might need to fix this because the isValidated clears stuff now
175
        (this.globalScope as any).isValidated = true;
2,663✔
176
    }
177

178

179
    private recursivelyAddNodeToSymbolTable(nodeData: SGNodeData) {
180
        if (!nodeData) {
497,981!
181
            return;
×
182
        }
183
        let nodeType: ComponentType;
184
        const nodeName = util.getSgNodeTypeName(nodeData.name);
497,981✔
185
        if (!this.globalScope.symbolTable.hasSymbol(nodeName, SymbolTypeFlag.typetime)) {
497,981✔
186
            let parentNode: ComponentType;
187
            if (nodeData.extends) {
258,311✔
188
                const parentNodeData = nodes[nodeData.extends.name.toLowerCase()];
239,670✔
189
                try {
239,670✔
190
                    parentNode = this.recursivelyAddNodeToSymbolTable(parentNodeData);
239,670✔
191
                } catch (error) {
192
                    this.logger.error(error, nodeData);
×
193
                }
194
            }
195
            nodeType = new ComponentType(nodeData.name, parentNode);
258,311✔
196
            nodeType.addBuiltInInterfaces();
258,311✔
197
            nodeType.isBuiltIn = true;
258,311✔
198
            if (nodeData.name === 'Node') {
258,311✔
199
                // Add `roSGNode` as shorthand for `roSGNodeNode`
200
                this.globalScope.symbolTable.addSymbol('roSGNode', { description: nodeData.description, isBuiltIn: true }, nodeType, SymbolTypeFlag.typetime);
2,663✔
201
            }
202
            this.globalScope.symbolTable.addSymbol(nodeName, { description: nodeData.description, isBuiltIn: true }, nodeType, SymbolTypeFlag.typetime);
258,311✔
203
        } else {
204
            nodeType = this.globalScope.symbolTable.getSymbolType(nodeName, { flags: SymbolTypeFlag.typetime }) as ComponentType;
239,670✔
205
        }
206

207
        return nodeType;
497,981✔
208
    }
209
    /**
210
     * Do all setup required for the global symbol table.
211
     */
212
    private populateGlobalSymbolTable() {
213
        //Setup primitive types in global symbolTable
214

215
        const builtInSymbolData: ExtraSymbolData = { isBuiltIn: true };
2,663✔
216

217
        this.globalScope.symbolTable.addSymbol('boolean', builtInSymbolData, BooleanType.instance, SymbolTypeFlag.typetime);
2,663✔
218
        this.globalScope.symbolTable.addSymbol('double', builtInSymbolData, DoubleType.instance, SymbolTypeFlag.typetime);
2,663✔
219
        this.globalScope.symbolTable.addSymbol('dynamic', builtInSymbolData, DynamicType.instance, SymbolTypeFlag.typetime);
2,663✔
220
        this.globalScope.symbolTable.addSymbol('float', builtInSymbolData, FloatType.instance, SymbolTypeFlag.typetime);
2,663✔
221
        this.globalScope.symbolTable.addSymbol('function', builtInSymbolData, FunctionType.instance, SymbolTypeFlag.typetime);
2,663✔
222
        this.globalScope.symbolTable.addSymbol('integer', builtInSymbolData, IntegerType.instance, SymbolTypeFlag.typetime);
2,663✔
223
        this.globalScope.symbolTable.addSymbol('longinteger', builtInSymbolData, LongIntegerType.instance, SymbolTypeFlag.typetime);
2,663✔
224
        this.globalScope.symbolTable.addSymbol('object', builtInSymbolData, ObjectType.instance, SymbolTypeFlag.typetime);
2,663✔
225
        this.globalScope.symbolTable.addSymbol('string', builtInSymbolData, StringType.instance, SymbolTypeFlag.typetime);
2,663✔
226
        this.globalScope.symbolTable.addSymbol('void', builtInSymbolData, VoidType.instance, SymbolTypeFlag.typetime);
2,663✔
227

228
        BuiltInInterfaceAdder.getLookupTable = () => this.globalScope.symbolTable;
753,848✔
229

230
        for (const callable of globalCallables) {
2,663✔
231
            this.globalScope.symbolTable.addSymbol(callable.name, { ...builtInSymbolData, description: callable.shortDescription }, callable.type, SymbolTypeFlag.runtime);
205,051✔
232
        }
233

234
        for (const ifaceData of Object.values(interfaces) as BRSInterfaceData[]) {
2,663✔
235
            const ifaceType = new InterfaceType(ifaceData.name);
242,333✔
236
            ifaceType.addBuiltInInterfaces();
242,333✔
237
            ifaceType.isBuiltIn = true;
242,333✔
238
            this.globalScope.symbolTable.addSymbol(ifaceData.name, { ...builtInSymbolData, description: ifaceData.description }, ifaceType, SymbolTypeFlag.typetime);
242,333✔
239
        }
240

241
        for (const componentData of Object.values(components) as BRSComponentData[]) {
2,663✔
242
            let roComponentType: BscType;
243
            const lowerComponentName = componentData.name.toLowerCase();
178,421✔
244

245
            if (lowerComponentName === 'rosgnode') {
178,421✔
246
                // we will add `roSGNode` as shorthand for `roSGNodeNode`, since all roSgNode components are SceneGraph nodes
247
                continue;
2,663✔
248
            }
249
            if (lowerComponentName === 'rofunction') {
175,758✔
250
                roComponentType = new roFunctionType();
2,663✔
251
            } else {
252
                roComponentType = new InterfaceType(componentData.name);
173,095✔
253
            }
254
            roComponentType.addBuiltInInterfaces();
175,758✔
255
            roComponentType.isBuiltIn = true;
175,758✔
256
            this.globalScope.symbolTable.addSymbol(componentData.name, { ...builtInSymbolData, description: componentData.description }, roComponentType, SymbolTypeFlag.typetime);
175,758✔
257
        }
258

259
        for (const nodeData of Object.values(nodes) as SGNodeData[]) {
2,663✔
260
            this.recursivelyAddNodeToSymbolTable(nodeData);
258,311✔
261
        }
262

263
        for (const eventData of Object.values(events) as BRSEventData[]) {
2,663✔
264
            const eventType = new InterfaceType(eventData.name);
47,934✔
265
            eventType.addBuiltInInterfaces();
47,934✔
266
            eventType.isBuiltIn = true;
47,934✔
267
            this.globalScope.symbolTable.addSymbol(eventData.name, { ...builtInSymbolData, description: eventData.description }, eventType, SymbolTypeFlag.typetime);
47,934✔
268
        }
269

270
    }
271

272
    /**
273
     * A graph of all files and their dependencies.
274
     * For example:
275
     *      File.xml -> [lib1.brs, lib2.brs]
276
     *      lib2.brs -> [lib3.brs] //via an import statement
277
     */
278
    private dependencyGraph = new DependencyGraph();
2,663✔
279

280
    public diagnostics: DiagnosticManager;
281

282
    /**
283
     * A scope that contains all built-in global functions.
284
     * All scopes should directly or indirectly inherit from this scope
285
     */
286
    public globalScope: Scope = undefined as any;
2,663✔
287

288
    /**
289
     * Plugins which can provide extra diagnostics or transform AST
290
     */
291
    public plugins: PluginInterface;
292

293
    private fileSymbolInformation = new Map<string, { provides: ProvidedSymbolInfo; requires: UnresolvedSymbol[] }>();
2,663✔
294

295
    private currentScopeValidationOptions: ScopeValidationOptions;
296

297
    /**
298
     *  Map of typetime symbols which depend upon the key symbol
299
     */
300
    private symbolDependencies = new Map<string, Set<string>>();
2,663✔
301

302

303
    /**
304
     * Symbol Table for storing custom component types
305
     * This is a sibling to the global table (as Components can be used/referenced anywhere)
306
     * Keeping custom components out of the global table and in a specific symbol table
307
     * compartmentalizes their use
308
     */
309
    private componentsTable = new SymbolTable('Custom Components');
2,663✔
310

311
    public addFileSymbolInfo(file: BrsFile) {
312
        this.fileSymbolInformation.set(file.pkgPath, {
2,480✔
313
            provides: file.providedSymbols,
314
            requires: file.requiredSymbols
315
        });
316
    }
317

318
    public getFileSymbolInfo(file: BrsFile) {
319
        return this.fileSymbolInformation.get(file.pkgPath);
2,485✔
320
    }
321

322
    /**
323
     * The path to bslib.brs (the BrightScript runtime for certain BrighterScript features)
324
     */
325
    public get bslibPkgPath() {
326
        //if there's an aliased (preferred) version of bslib from roku_modules loaded into the program, use that
327
        if (this.getFile(bslibAliasedRokuModulesPkgPath)) {
3,153✔
328
            return bslibAliasedRokuModulesPkgPath;
11✔
329

330
            //if there's a non-aliased version of bslib from roku_modules, use that
331
        } else if (this.getFile(bslibNonAliasedRokuModulesPkgPath)) {
3,142✔
332
            return bslibNonAliasedRokuModulesPkgPath;
24✔
333

334
            //default to the embedded version
335
        } else {
336
            return `${this.options.bslibDestinationDir}${path.sep}bslib.brs`;
3,118✔
337
        }
338
    }
339

340
    public get bslibPrefix() {
341
        if (this.bslibPkgPath === bslibNonAliasedRokuModulesPkgPath) {
2,280✔
342
            return 'rokucommunity_bslib';
18✔
343
        } else {
344
            return 'bslib';
2,262✔
345
        }
346
    }
347

348

349
    /**
350
     * A map of every file loaded into this program, indexed by its original file location
351
     */
352
    public files = {} as Record<string, BscFile>;
2,663✔
353
    /**
354
     * A map of every file loaded into this program, indexed by its destPath
355
     */
356
    private destMap = new Map<string, BscFile>();
2,663✔
357
    /**
358
     * Plugins can contribute multiple virtual files for a single physical file.
359
     * This collection links the virtual files back to the physical file that produced them.
360
     * The key is the standardized and lower-cased srcPath
361
     */
362
    private fileClusters = new Map<string, BscFile[]>();
2,663✔
363

364
    /**
365
     * Map from a lower-cased namespace name part to the set of `BrsFile`s that contribute
366
     * to it. Built lazily, invalidated whenever any file is added, removed, or re-parsed
367
     * (`setFile` and `removeFile` both clear it).
368
     *
369
     * Used by `ScopeNamespaceLookup` to resolve a namespace name to its contributing
370
     * files in O(1), then intersect against the scope's file set.
371
     */
372
    private namespaceContributors: Map<string, Set<BrsFile>> | undefined;
373

374
    /**
375
     * Look up the set of `BrsFile`s that declare any part of the given namespace name
376
     * (lowercased). Returns `undefined` when no file contributes.
377
     * @internal
378
     */
379
    protected getNamespaceContributors(namespaceNameLower: string): Set<BrsFile> | undefined {
380
        if (!this.namespaceContributors) {
1,271✔
381
            this.namespaceContributors = this.buildNamespaceContributors();
408✔
382
        }
383
        return this.namespaceContributors.get(namespaceNameLower);
1,271✔
384
    }
385

386
    private buildNamespaceContributors(): Map<string, Set<BrsFile>> {
387
        const contributors = new Map<string, Set<BrsFile>>();
408✔
388
        for (const file of Object.values(this.files)) {
408✔
389
            if (isBrsFile(file)) {
1,041✔
390
                // eslint-disable-next-line @typescript-eslint/dot-notation
391
                for (const nameLower of file['getNamespaceContributions']().keys()) {
775✔
392
                    let set = contributors.get(nameLower);
1,161✔
393
                    if (!set) {
1,161✔
394
                        set = new Set<BrsFile>();
664✔
395
                        contributors.set(nameLower, set);
664✔
396
                    }
397
                    set.add(file);
1,161✔
398
                }
399
            }
400
        }
401
        return contributors;
408✔
402
    }
403

404
    /**
405
     * Cached slow-path namespace aggregates, keyed by `(nameLower, sorted-contributor-pkgPaths)`.
406
     * Two scopes with the same in-scope file set for a multi-contributor namespace share
407
     * the same aggregate object (and therefore the same merged statement collections and
408
     * symbolTable instance). Built lazily, invalidated alongside `namespaceContributors`.
409
     *
410
     * The aggregate is stored as a `NamespaceContainer` whose `namespaces` field is an
411
     * empty Map: scopes always wrap the aggregate before returning to plugins, and the
412
     * wrapper supplies its own scope-filtered children. Plugins never see the aggregate
413
     * directly.
414
     */
415
    private aggregateNamespaceContainerCache: Map<string, NamespaceContainer> | undefined;
416

417
    /**
418
     * Get or build the shared aggregate for a namespace whose in-scope contributors
419
     * include more than one file. The aggregate's heavy fields are computed once per
420
     * unique `(nameLower, contributing-files-set)` and reused across every scope that
421
     * sees the same set.
422
     * @internal
423
     */
424
    protected getAggregateNamespaceContainer(nameLower: string, contributions: NamespaceFileContribution[]): NamespaceContainer {
425
        if (!this.aggregateNamespaceContainerCache) {
411✔
426
            this.aggregateNamespaceContainerCache = new Map<string, NamespaceContainer>();
55✔
427
        }
428
        //sorted pkgPaths ensure two scopes with the same contributor set hit the same key
429
        const key = nameLower + '|' + contributions
411✔
430
            .map(c => c.file.pkgPath.toLowerCase())
830✔
431
            .sort()
432
            .join('|');
433
        let aggregate = this.aggregateNamespaceContainerCache.get(key);
411✔
434
        if (!aggregate) {
411✔
435
            aggregate = this.buildAggregateNamespaceContainer(contributions);
263✔
436
            this.aggregateNamespaceContainerCache.set(key, aggregate);
263✔
437
        }
438
        return aggregate;
411✔
439
    }
440

441
    private buildAggregateNamespaceContainer(contributions: NamespaceFileContribution[]): NamespaceContainer {
442
        const first = contributions[0];
263✔
443
        //field order matches the NamespaceContainer interface declaration so aggregates
444
        //share a single V8 hidden class with the per-scope wrapper containers
445
        const aggregate: NamespaceContainer = {
263✔
446
            file: first.file,
447
            fullName: first.fullName,
448
            nameRange: first.nameRange,
449
            lastPartName: first.lastPartName,
450
            namespaces: new Map(),
451
            namespaceStatements: undefined,
452
            statements: undefined,
453
            classStatements: undefined,
454
            functionStatements: undefined,
455
            enumStatements: undefined,
456
            constStatements: undefined,
457
            symbolTable: undefined
458
        };
459
        for (const contribution of contributions) {
263✔
460
            if (contribution.namespaceStatements?.length) {
532✔
461
                (aggregate.namespaceStatements ??= []).push(...contribution.namespaceStatements);
515✔
462
            }
463
            if (contribution.statements?.length) {
532✔
464
                (aggregate.statements ??= []).push(...contribution.statements);
515✔
465
            }
466
            if (contribution.classStatements) {
532✔
467
                aggregate.classStatements = { ...(aggregate.classStatements ?? {}), ...contribution.classStatements };
23✔
468
            }
469
            if (contribution.functionStatements) {
532✔
470
                aggregate.functionStatements = { ...(aggregate.functionStatements ?? {}), ...contribution.functionStatements };
265✔
471
            }
472
            if (contribution.enumStatements) {
532✔
473
                aggregate.enumStatements ??= new Map();
9!
474
                for (const [key, value] of contribution.enumStatements) {
9✔
475
                    aggregate.enumStatements.set(key, value);
9✔
476
                }
477
            }
478
            if (contribution.constStatements) {
532✔
479
                aggregate.constStatements ??= new Map();
214✔
480
                for (const [key, value] of contribution.constStatements) {
214✔
481
                    aggregate.constStatements.set(key, value);
218✔
482
                }
483
            }
484
            if (contribution.symbolTable) {
532✔
485
                aggregate.symbolTable ??= new SymbolTable(`Namespace Multi-File Aggregate: '${first.fullName}'`);
515✔
486
                aggregate.symbolTable.mergeSymbolTable(contribution.symbolTable);
515✔
487
            }
488
        }
489
        return aggregate;
263✔
490
    }
491

492
    /**
493
     * Invalidate the program-level namespace contributors map and the slow-path aggregate
494
     * cache. Called by `setFile` and `removeFile`; downstream scope namespace lookups
495
     * already rebuild via the dependency-graph invalidation chain, so this only needs
496
     * to drop the cached maps.
497
     */
498
    private invalidateNamespaceContributorCache() {
499
        this.namespaceContributors = undefined;
3,671✔
500
        this.aggregateNamespaceContainerCache = undefined;
3,671✔
501
    }
502

503
    private scopes = {} as Record<string, Scope>;
2,663✔
504

505
    protected addScope(scope: Scope) {
506
        this.scopes[scope.name] = scope;
2,795✔
507
        delete this.sortedScopeNames;
2,795✔
508
    }
509

510
    protected removeScope(scope: Scope) {
511
        if (this.scopes[scope.name]) {
16!
512
            delete this.scopes[scope.name];
16✔
513
            delete this.sortedScopeNames;
16✔
514
        }
515
    }
516

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

525
    /**
526
     * Get the component with the specified name
527
     */
528
    public getComponent(componentName: string) {
529
        if (componentName) {
3,686✔
530
            //return the first compoment in the list with this name
531
            //(components are ordered in this list by destPath to ensure consistency)
532
            return this.components[componentName.toLowerCase()]?.[0];
3,640✔
533
        } else {
534
            return undefined;
46✔
535
        }
536
    }
537

538
    /**
539
     * Get the sorted names of custom components
540
     */
541
    public getSortedComponentNames() {
542
        const componentNames = Object.keys(this.components);
2,233✔
543
        componentNames.sort((a, b) => {
2,233✔
544
            if (a < b) {
883✔
545
                return -1;
308✔
546
            } else if (b < a) {
575!
547
                return 1;
575✔
548
            }
549
            return 0;
×
550
        });
551
        return componentNames;
2,233✔
552
    }
553

554
    /**
555
     * Keeps a set of all the components that need to have their types updated during the current validation cycle
556
     * Map <componentKey, componentName>
557
     */
558
    private componentSymbolsToUpdate = new Map<string, string>();
2,663✔
559

560
    /**
561
     * Register (or replace) the reference to a component in the component map
562
     */
563
    private registerComponent(xmlFile: XmlFile, scope: XmlScope) {
564
        const key = this.getComponentKey(xmlFile);
542✔
565
        if (!this.components[key]) {
542✔
566
            this.components[key] = [];
525✔
567
        }
568
        this.components[key].push({
542✔
569
            file: xmlFile,
570
            scope: scope
571
        });
572
        this.components[key].sort((a, b) => {
542✔
573
            const pathA = a.file.destPath.toLowerCase();
5✔
574
            const pathB = b.file.destPath.toLowerCase();
5✔
575
            if (pathA < pathB) {
5✔
576
                return -1;
1✔
577
            } else if (pathA > pathB) {
4!
578
                return 1;
4✔
579
            }
580
            return 0;
×
581
        });
582
        this.syncComponentDependencyGraph(this.components[key]);
542✔
583
        this.addDeferredComponentTypeSymbolCreation(xmlFile);
542✔
584
    }
585

586
    /**
587
     * Remove the specified component from the components map
588
     */
589
    private unregisterComponent(xmlFile: XmlFile) {
590
        const key = this.getComponentKey(xmlFile);
16✔
591
        const arr = this.components[key] || [];
16!
592
        for (let i = 0; i < arr.length; i++) {
16✔
593
            if (arr[i].file === xmlFile) {
16!
594
                arr.splice(i, 1);
16✔
595
                break;
16✔
596
            }
597
        }
598

599
        this.syncComponentDependencyGraph(arr);
16✔
600
        this.addDeferredComponentTypeSymbolCreation(xmlFile);
16✔
601
    }
602

603
    /**
604
     * Adds a component described in an XML to the set of components that needs to be updated this validation cycle.
605
     * @param xmlFile XML file with <component> tag
606
     */
607
    private addDeferredComponentTypeSymbolCreation(xmlFile: XmlFile) {
608
        const componentKey = this.getComponentKey(xmlFile);
1,049✔
609
        const componentName = xmlFile.componentName?.text;
1,049✔
610
        if (this.componentSymbolsToUpdate.has(componentKey)) {
1,049✔
611
            return;
511✔
612
        }
613
        this.componentSymbolsToUpdate.set(componentKey, componentName);
538✔
614
    }
615

616
    private getComponentKey(xmlFile: XmlFile) {
617
        return (xmlFile.componentName?.text ?? xmlFile.pkgPath).toLowerCase();
1,607✔
618
    }
619

620
    /**
621
     * Resolves symbol table with the first component in this.components to have the same name as the component in the file
622
     * @param componentKey key getting a component from `this.components`
623
     * @param componentName the unprefixed name of the component that will be added (e.g. 'MyLabel' NOT 'roSgNodeMyLabel')
624
     */
625
    private updateComponentSymbolInGlobalScope(componentKey: string, componentName: string) {
626
        const symbolName = componentName ? util.getSgNodeTypeName(componentName) : undefined;
489✔
627
        if (!symbolName) {
489✔
628
            return;
10✔
629
        }
630
        const components = this.components[componentKey] || [];
479!
631
        const previousComponentType = this.componentsTable.getSymbolType(symbolName, { flags: SymbolTypeFlag.typetime });
479✔
632
        // Remove any existing symbols that match
633
        this.componentsTable.removeSymbol(symbolName);
479✔
634
        if (components.length > 0) {
479✔
635
            // There is a component that can be added - use it.
636
            const componentScope = components[0].scope;
478✔
637

638
            this.componentsTable.removeSymbol(symbolName);
478✔
639
            componentScope.linkSymbolTable();
478✔
640
            const componentType = componentScope.getComponentType();
478✔
641
            if (componentType) {
478!
642
                this.componentsTable.addSymbol(symbolName, {}, componentType, SymbolTypeFlag.typetime);
478✔
643
            }
644
            const typeData = {};
478✔
645
            const isSameAsPrevious = previousComponentType && componentType.isEqual(previousComponentType, typeData);
478✔
646
            const isComponentTypeDifferent = !previousComponentType || isReferenceType(previousComponentType) || !isSameAsPrevious;
478✔
647
            componentScope.unlinkSymbolTable();
478✔
648
            return isComponentTypeDifferent;
478✔
649

650
        }
651
        // There was a previous component type, but no new one, so it's different
652
        return !!previousComponentType;
1✔
653
    }
654

655
    /**
656
     * 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
657
     * This is so on a first validation, these types can be resolved in teh future (eg. when the actual component is created)
658
     * If we don't add reference types at this top level, they will be created at the file level, and will never get resolved
659
     * @param componentKey key getting a component from `this.components`
660
     * @param componentName the unprefixed name of the component that will be added (e.g. 'MyLabel' NOT 'roSgNodeMyLabel')
661
     */
662
    private addComponentReferenceType(componentKey: string, componentName: string) {
663
        const symbolName = componentName ? util.getSgNodeTypeName(componentName) : undefined;
485✔
664
        if (!symbolName) {
485✔
665
            return;
10✔
666
        }
667
        const components = this.components[componentKey] || [];
475!
668

669
        if (components.length > 0) {
475✔
670
            // There is a component that can be added,
671
            if (!this.componentsTable.hasSymbol(symbolName, SymbolTypeFlag.typetime)) {
474✔
672
                // it doesn't already exist in the table
673
                const componentRefType = new ReferenceType(symbolName, symbolName, SymbolTypeFlag.typetime, () => this.componentsTable);
8,380✔
674
                if (componentRefType) {
467!
675
                    this.componentsTable.addSymbol(symbolName, {}, componentRefType, SymbolTypeFlag.typetime);
467✔
676
                }
677
            }
678
        } else {
679
            // there is no component. remove from table
680
            this.componentsTable.removeSymbol(symbolName);
1✔
681
        }
682
    }
683

684
    /**
685
     * re-attach the dependency graph with a new key for any component who changed
686
     * their position in their own named array (only matters when there are multiple
687
     * components with the same name)
688
     */
689
    private syncComponentDependencyGraph(components: Array<{ file: XmlFile; scope: XmlScope }>) {
690
        //reattach every dependency graph
691
        for (let i = 0; i < components.length; i++) {
558✔
692
            const { file, scope } = components[i];
548✔
693

694
            //attach (or re-attach) the dependencyGraph for every component whose position changed
695
            if (file.dependencyGraphIndex !== i) {
548✔
696
                file.dependencyGraphIndex = i;
544✔
697
                this.dependencyGraph.addOrReplace(file.dependencyGraphKey, file.dependencies);
544✔
698
                file.attachDependencyGraph(this.dependencyGraph);
544✔
699
                scope.attachDependencyGraph(this.dependencyGraph);
544✔
700
            }
701
        }
702
    }
703

704
    /**
705
     * Get a list of all files that are included in the project but are not referenced
706
     * by any scope in the program.
707
     */
708
    public getUnreferencedFiles() {
709
        let result = [] as BscFile[];
×
710
        for (let filePath in this.files) {
×
711
            let file = this.files[filePath];
×
712
            //is this file part of a scope
713
            if (!this.getFirstScopeForFile(file)) {
×
714
                //no scopes reference this file. add it to the list
715
                result.push(file);
×
716
            }
717
        }
718
        return result;
×
719
    }
720

721
    /**
722
     * Get the list of errors for the entire program.
723
     */
724
    public getDiagnostics() {
725
        return this.diagnostics.getDiagnostics();
1,794✔
726
    }
727

728
    /**
729
     * Determine if the specified file is loaded in this program right now.
730
     * @param filePath the absolute or relative path to the file
731
     * @param normalizePath should the provided path be normalized before use
732
     */
733
    public hasFile(filePath: string, normalizePath = true) {
3,730✔
734
        return !!this.getFile(filePath, normalizePath);
3,730✔
735
    }
736

737
    /**
738
     * roku filesystem is case INsensitive, so find the scope by key case insensitive
739
     * @param scopeName xml scope names are their `destPath`. Source scope is stored with the key `"source"`
740
     */
741
    public getScopeByName(scopeName: string): Scope | undefined {
742
        if (!scopeName) {
83!
743
            return undefined;
×
744
        }
745
        //most scopes are xml file pkg paths. however, the ones that are not are single names like "global" and "scope",
746
        //so it's safe to run the standardizePkgPath method
747
        scopeName = s`${scopeName}`;
83✔
748
        let key = Object.keys(this.scopes).find(x => x.toLowerCase() === scopeName.toLowerCase());
192✔
749
        return this.scopes[key!];
83✔
750
    }
751

752
    /**
753
     * Return all scopes
754
     */
755
    public getScopes() {
756
        return Object.values(this.scopes);
13✔
757
    }
758

759
    /**
760
     * Find the scope for the specified component
761
     */
762
    public getComponentScope(componentName: string) {
763
        return this.getComponent(componentName)?.scope;
1,045✔
764
    }
765

766
    /**
767
     * Update internal maps with this file reference
768
     */
769
    private assignFile<T extends BscFile = BscFile>(file: T) {
770
        const fileAddEvent: BeforeAddFileEvent = {
3,428✔
771
            file: file,
772
            program: this
773
        };
774
        this.scopesPerFile.clear();
3,428✔
775

776
        this.plugins.emit('beforeAddFile', fileAddEvent);
3,428✔
777

778
        this.files[file.srcPath.toLowerCase()] = file;
3,428✔
779
        this.destMap.set(file.destPath.toLowerCase(), file);
3,428✔
780

781
        this.plugins.emit('afterAddFile', fileAddEvent);
3,428✔
782

783
        return file;
3,428✔
784
    }
785

786
    /**
787
     * Remove this file from internal maps
788
     */
789
    private unassignFile<T extends BscFile = BscFile>(file: T) {
790
        delete this.files[file.srcPath.toLowerCase()];
249✔
791
        this.destMap.delete(file.destPath.toLowerCase());
249✔
792
        this.scopesPerFile.clear();
249✔
793
        return file;
249✔
794
    }
795

796
    /**
797
     * Load a file into the program. If that file already exists, it is replaced.
798
     * If file contents are provided, those are used, Otherwise, the file is loaded from the file system
799
     * @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:/`)
800
     * @param fileData the file contents. omit or pass `undefined` to prevent loading the data at this time
801
     */
802
    public setFile<T extends BscFile>(srcDestOrPkgPath: string, fileData?: FileData): T;
803
    /**
804
     * Load a file into the program. If that file already exists, it is replaced.
805
     * @param fileEntry an object that specifies src and dest for the file.
806
     * @param fileData the file contents. omit or pass `undefined` to prevent loading the data at this time
807
     */
808
    public setFile<T extends BscFile>(fileEntry: FileObj, fileData: FileData): T;
809
    public setFile<T extends BscFile>(fileParam: FileObj | string, fileData: FileData): T {
810
        //normalize the file paths
811
        const { srcPath, destPath } = this.getPaths(fileParam, this.options.rootDir);
3,424✔
812

813
        //namespace contributions for the new/replaced file may differ; force the
814
        //program-level contributors map to rebuild on next query
815
        this.invalidateNamespaceContributorCache();
3,424✔
816

817
        let file = this.logger.time(LogLevel.debug, ['Program.setFile()', chalk.green(srcPath)], () => {
3,424✔
818
            //if the file is already loaded, remove it
819
            if (this.hasFile(srcPath)) {
3,424✔
820
                this.removeFile(srcPath, true, true);
225✔
821
            }
822

823
            const data = new LazyFileData(fileData);
3,424✔
824

825
            const event = new ProvideFileEventInternal(this, srcPath, destPath, data, this.fileFactory);
3,424✔
826

827
            this.plugins.emit('beforeProvideFile', event);
3,424✔
828
            this.plugins.emit('provideFile', event);
3,424✔
829
            this.plugins.emit('afterProvideFile', event);
3,424✔
830

831
            //if no files were provided, create a AssetFile to represent it.
832
            if (event.files.length === 0) {
3,424✔
833
                event.files.push(
47✔
834
                    this.fileFactory.AssetFile({
835
                        srcPath: event.srcPath,
836
                        destPath: event.destPath,
837
                        pkgPath: event.destPath,
838
                        data: data
839
                    })
840
                );
841
            }
842

843
            //find the file instance for the srcPath that triggered this action.
844
            const primaryFile = event.files.find(x => x.srcPath === srcPath);
3,424✔
845

846
            if (!primaryFile) {
3,424!
847
                throw new Error(`No file provided for srcPath '${srcPath}'. Instead, received ${JSON.stringify(event.files.map(x => ({
×
848
                    type: x.type,
849
                    srcPath: x.srcPath,
850
                    destPath: x.destPath
851
                })))}`);
852
            }
853

854
            //link the virtual files to the primary file
855
            this.fileClusters.set(primaryFile.srcPath?.toLowerCase(), event.files);
3,424!
856

857
            for (const file of event.files) {
3,424✔
858
                file.srcPath = s(file.srcPath);
3,428✔
859
                if (file.destPath) {
3,428!
860
                    file.destPath = s`${util.replaceCaseInsensitive(file.destPath, this.options.rootDir, '')}`;
3,428✔
861
                }
862
                if (file.pkgPath) {
3,428✔
863
                    file.pkgPath = s`${util.replaceCaseInsensitive(file.pkgPath, this.options.rootDir, '')}`;
3,424✔
864
                } else {
865
                    file.pkgPath = file.destPath;
4✔
866
                }
867
                file.excludeFromOutput = file.excludeFromOutput === true;
3,428✔
868

869
                //set the dependencyGraph key for every file to its destPath
870
                file.dependencyGraphKey = file.destPath.toLowerCase();
3,428✔
871

872
                this.assignFile(file);
3,428✔
873

874
                //register a callback anytime this file's dependencies change
875
                if (typeof file.onDependenciesChanged === 'function') {
3,428✔
876
                    file.disposables ??= [];
3,373!
877
                    file.disposables.push(
3,373✔
878
                        this.dependencyGraph.onchange(file.dependencyGraphKey, file.onDependenciesChanged.bind(file))
879
                    );
880
                }
881

882
                //register this file (and its dependencies) with the dependency graph
883
                this.dependencyGraph.addOrReplace(file.dependencyGraphKey, file.dependencies ?? []);
3,428✔
884

885
                //if this is a `source` file, add it to the source scope's dependency list
886
                if (this.isSourceBrsFile(file)) {
3,428✔
887
                    this.createSourceScope();
2,372✔
888
                    this.dependencyGraph.addDependency('scope:source', file.dependencyGraphKey);
2,372✔
889
                }
890

891
                //if this is an xml file in the components folder, register it as a component
892
                if (this.isComponentsXmlFile(file)) {
3,428✔
893
                    this.plugins.emit('beforeProvideScope', {
542✔
894
                        program: this,
895
                        scope: undefined
896
                    });
897
                    //create a new scope for this xml file
898
                    let scope = new XmlScope(file, this);
542✔
899
                    this.addScope(scope);
542✔
900

901
                    //register this componet now that we have parsed it and know its component name
902
                    this.registerComponent(file, scope);
542✔
903
                    this.plugins.emit('provideScope', {
542✔
904
                        program: this,
905
                        scope: scope
906
                    });
907

908
                    //notify plugins that the scope is created and the component is registered
909
                    this.plugins.emit('afterProvideScope', {
542✔
910
                        program: this,
911
                        scope: scope
912
                    });
913
                }
914
            }
915

916
            return primaryFile;
3,424✔
917
        });
918
        return file as T;
3,424✔
919
    }
920

921
    /**
922
     * Given a srcPath, a destPath, or both, resolve whichever is missing, relative to rootDir.
923
     * @param fileParam an object representing file paths
924
     * @param rootDir must be a pre-normalized path
925
     */
926
    private getPaths(fileParam: string | FileObj | { srcPath?: string; pkgPath?: string }, rootDir: string) {
927
        let srcPath: string | undefined;
928
        let destPath: string | undefined;
929

930
        assert.ok(fileParam, 'fileParam is required');
3,676✔
931

932
        //lift the path vars from the incoming param
933
        if (typeof fileParam === 'string') {
3,676✔
934
            fileParam = this.removePkgPrefix(fileParam);
3,067✔
935
            srcPath = s`${path.resolve(rootDir, fileParam)}`;
3,067✔
936
            destPath = s`${util.replaceCaseInsensitive(srcPath, rootDir, '')}`;
3,067✔
937
        } else {
938
            let param: any = fileParam;
609✔
939

940
            if (param.src) {
609✔
941
                srcPath = s`${param.src}`;
608✔
942
            }
943
            if (param.srcPath) {
609!
944
                srcPath = s`${param.srcPath}`;
×
945
            }
946
            if (param.dest) {
609✔
947
                destPath = s`${this.removePkgPrefix(param.dest)}`;
608✔
948
            }
949
            if (param.pkgPath) {
609!
950
                destPath = s`${this.removePkgPrefix(param.pkgPath)}`;
×
951
            }
952
        }
953

954
        //if there's no srcPath, use the destPath to build an absolute srcPath
955
        if (!srcPath) {
3,676✔
956
            srcPath = s`${rootDir}/${destPath}`;
1✔
957
        }
958
        //coerce srcPath to an absolute path
959
        if (!path.isAbsolute(srcPath)) {
3,676✔
960
            srcPath = util.standardizePath(srcPath);
1✔
961
        }
962

963
        //if destPath isn't set, compute it from the other paths
964
        if (!destPath) {
3,676✔
965
            destPath = s`${util.replaceCaseInsensitive(srcPath, rootDir, '')}`;
1✔
966
        }
967

968
        assert.ok(srcPath, 'fileEntry.src is required');
3,676✔
969
        assert.ok(destPath, 'fileEntry.dest is required');
3,676✔
970

971
        return {
3,676✔
972
            srcPath: srcPath,
973
            //remove leading slash
974
            destPath: destPath.replace(/^[\/\\]+/, '')
975
        };
976
    }
977

978
    /**
979
     * Remove any leading `pkg:/` found in the path
980
     */
981
    private removePkgPrefix(path: string) {
982
        return path.replace(/^pkg:\//i, '');
3,675✔
983
    }
984

985
    /**
986
     * Is this file a .brs file found somewhere within the `pkg:/source/` folder?
987
     */
988
    private isSourceBrsFile(file: BscFile) {
989
        return !!/^(pkg:\/)?source[\/\\]/.exec(file.destPath);
3,677✔
990
    }
991

992
    /**
993
     * Is this file a .brs file found somewhere within the `pkg:/source/` folder?
994
     */
995
    private isComponentsXmlFile(file: BscFile): file is XmlFile {
996
        return isXmlFile(file) && !!/^(pkg:\/)?components[\/\\]/.exec(file.destPath);
3,428✔
997
    }
998

999
    /**
1000
     * Ensure source scope is created.
1001
     * Note: automatically called internally, and no-op if it exists already.
1002
     */
1003
    public createSourceScope() {
1004
        if (!this.scopes.source) {
3,450✔
1005
            const sourceScope = new Scope('source', this, 'scope:source');
2,253✔
1006
            sourceScope.attachDependencyGraph(this.dependencyGraph);
2,253✔
1007
            this.addScope(sourceScope);
2,253✔
1008
            this.plugins.emit('afterProvideScope', {
2,253✔
1009
                program: this,
1010
                scope: sourceScope
1011
            });
1012
        }
1013
    }
1014

1015
    /**
1016
     * Remove a set of files from the program
1017
     * @param srcPaths can be an array of srcPath or destPath strings
1018
     * @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
1019
     */
1020
    public removeFiles(srcPaths: string[], normalizePath = true) {
1✔
1021
        for (let srcPath of srcPaths) {
1✔
1022
            this.removeFile(srcPath, normalizePath);
1✔
1023
        }
1024
    }
1025

1026
    /**
1027
     * Remove a file from the program
1028
     * @param filePath can be a srcPath, a destPath, or a destPath with leading `pkg:/`
1029
     * @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
1030
     */
1031
    public removeFile(filePath: string, normalizePath = true, keepSymbolInformation = false) {
40✔
1032
        this.logger.debug('Program.removeFile()', filePath);
247✔
1033
        const paths = this.getPaths(filePath, this.options.rootDir);
247✔
1034

1035
        //namespace contributions may have included this file; force the program-level
1036
        //contributors map to rebuild on next query
1037
        this.invalidateNamespaceContributorCache();
247✔
1038

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

1042
        for (const file of files) {
247✔
1043
            //if a file has already been removed, nothing more needs to be done here
1044
            if (!file || !this.hasFile(file.srcPath)) {
250✔
1045
                continue;
1✔
1046
            }
1047
            this.diagnostics.clearForFile(file.srcPath);
249✔
1048

1049
            const event: BeforeRemoveFileEvent = { file: file, program: this };
249✔
1050
            this.plugins.emit('beforeRemoveFile', event);
249✔
1051

1052
            //if there is a scope named the same as this file's path, remove it (i.e. xml scopes)
1053
            let scope = this.scopes[file.destPath];
249✔
1054
            if (scope) {
249✔
1055
                this.logger.debug('Removing associated scope', scope.name);
16✔
1056
                const scopeRemoveEvent = {
16✔
1057
                    program: this,
1058
                    scope: scope
1059
                };
1060
                this.plugins.emit('beforeRemoveScope', scopeRemoveEvent);
16✔
1061
                this.plugins.emit('removeScope', scopeRemoveEvent);
16✔
1062
                scope.dispose();
16✔
1063
                //notify dependencies of this scope that it has been removed
1064
                this.dependencyGraph.remove(scope.dependencyGraphKey!);
16✔
1065
                this.removeScope(this.scopes[file.destPath]);
16✔
1066
                this.plugins.emit('afterRemoveScope', scopeRemoveEvent);
16✔
1067
            }
1068
            //remove the file from the program
1069
            this.unassignFile(file);
249✔
1070

1071
            this.dependencyGraph.remove(file.dependencyGraphKey);
249✔
1072

1073
            //if this is a pkg:/source file, notify the `source` scope that it has changed
1074
            if (this.isSourceBrsFile(file)) {
249✔
1075
                this.dependencyGraph.removeDependency('scope:source', file.dependencyGraphKey);
177✔
1076
            }
1077
            if (isBrsFile(file)) {
249✔
1078
                this.logger.debug('Removing file symbol info', file.srcPath);
225✔
1079

1080
                if (!keepSymbolInformation) {
225✔
1081
                    this.fileSymbolInformation.delete(file.pkgPath);
15✔
1082
                }
1083
                this.crossScopeValidation.clearResolutionsForFile(file);
225✔
1084
            }
1085

1086
            this.diagnostics.clearForFile(file.srcPath);
249✔
1087

1088
            //if this is a component, remove it from our components map
1089
            if (isXmlFile(file)) {
249✔
1090
                this.logger.debug('Unregistering component', file.srcPath);
16✔
1091

1092
                this.unregisterComponent(file);
16✔
1093
            }
1094
            this.logger.debug('Disposing file', file.srcPath);
249✔
1095

1096
            //dispose any disposable things on the file
1097
            for (const disposable of file?.disposables ?? []) {
249!
1098
                disposable();
241✔
1099
            }
1100
            //dispose file
1101
            file?.dispose?.();
249!
1102

1103
            this.plugins.emit('afterRemoveFile', event);
249✔
1104
        }
1105
    }
1106

1107
    public crossScopeValidation = new CrossScopeValidator(this);
2,663✔
1108

1109
    private isFirstValidation = true;
2,663✔
1110

1111
    private validationDetails: {
2,663✔
1112
        brsFilesValidated: BrsFile[];
1113
        xmlFilesValidated: XmlFile[];
1114
        changedSymbols: Map<SymbolTypeFlag, Set<string>>;
1115
        changedComponentTypes: string[];
1116
        scopesToInvalidate: Scope[];
1117
        scopesToValidate: Scope[];
1118
        filesToBeValidatedInScopeContext: Set<BscFile>;
1119

1120
    } = {
1121
            brsFilesValidated: [],
1122
            xmlFilesValidated: [],
1123
            changedSymbols: new Map<SymbolTypeFlag, Set<string>>(),
1124
            changedComponentTypes: [],
1125
            scopesToInvalidate: [],
1126
            scopesToValidate: [],
1127
            filesToBeValidatedInScopeContext: new Set<BscFile>()
1128
        };
1129

1130
    public lastValidationInfo: {
2,663✔
1131
        brsFilesSrcPath: Set<string>;
1132
        xmlFilesSrcPath: Set<string>;
1133
        scopeNames: Set<string>;
1134
        componentsRebuilt: Set<string>;
1135
    } = {
1136
            brsFilesSrcPath: new Set<string>(),
1137
            xmlFilesSrcPath: new Set<string>(),
1138
            scopeNames: new Set<string>(),
1139
            componentsRebuilt: new Set<string>()
1140
        };
1141

1142
    /**
1143
     * Counter used to track which validation run is being logged
1144
     */
1145
    private validationRunSequence = 1;
2,663✔
1146

1147
    /**
1148
     * How many milliseconds can pass while doing synchronous operations in validate before we register a short timeout (i.e. yield to the event loop)
1149
     */
1150
    private validationMinSyncDuration = 75;
2,663✔
1151

1152
    private validatePromise: Promise<void> | undefined;
1153

1154
    /**
1155
     * Traverse the entire project, and validate all scopes
1156
     */
1157
    public validate(): void;
1158
    public validate(options: { async: false; cancellationToken?: CancellationToken }): void;
1159
    public validate(options: { async: true; cancellationToken?: CancellationToken }): Promise<void>;
1160
    public validate(options?: { async?: boolean; cancellationToken?: CancellationToken }) {
1161
        const validationRunId = this.validationRunSequence++;
2,291✔
1162

1163
        let previousValidationPromise = this.validatePromise;
2,291✔
1164
        const deferred = new Deferred();
2,291✔
1165

1166
        if (options?.async) {
2,291✔
1167
            //we're async, so create a new promise chain to resolve after this validation is done
1168
            this.validatePromise = Promise.resolve(previousValidationPromise).then(() => {
368✔
1169
                return deferred.promise;
368✔
1170
            });
1171

1172
            //we are not async but there's a pending promise, then we cannot run this validation
1173
        } else if (previousValidationPromise !== undefined) {
1,923!
1174
            throw new Error('Cannot run synchronous validation while an async validation is in progress');
×
1175
        }
1176

1177
        let beforeValidateProgramWasEmitted = false;
2,291✔
1178

1179
        const brsFilesValidated: BrsFile[] = this.validationDetails.brsFilesValidated;
2,291✔
1180
        const xmlFilesValidated: XmlFile[] = this.validationDetails.xmlFilesValidated;
2,291✔
1181
        const changedSymbols = this.validationDetails.changedSymbols;
2,291✔
1182
        const changedComponentTypes = this.validationDetails.changedComponentTypes;
2,291✔
1183
        const scopesToInvalidate = this.validationDetails.scopesToInvalidate;
2,291✔
1184
        const scopesToValidate = this.validationDetails.scopesToValidate;
2,291✔
1185
        const filesToBeValidatedInScopeContext = this.validationDetails.filesToBeValidatedInScopeContext;
2,291✔
1186

1187
        //validate every file
1188

1189
        let logValidateEnd = (status?: string) => { };
2,291✔
1190

1191
        //will be populated later on during the corresponding sequencer event
1192
        let filesToProcess: BscFile[];
1193

1194
        const sequencer = new Sequencer({
2,291✔
1195
            name: 'program.validate',
1196
            cancellationToken: options?.cancellationToken ?? new CancellationTokenSource().token,
13,746✔
1197
            minSyncDuration: this.validationMinSyncDuration
1198
        });
1199
        //this sequencer allows us to run in both sync and async mode, depending on whether options.async is enabled.
1200
        //We use this to prevent starving the CPU during long validate cycles when running in a language server context
1201
        sequencer
2,291✔
1202
            .once('wait for previous run', () => {
1203
                //if running in async mode, return the previous validation promise to ensure we're only running one at a time
1204
                if (options?.async) {
2,291✔
1205
                    return previousValidationPromise;
368✔
1206
                }
1207
            })
1208
            .once('before and on programValidate', () => {
1209
                logValidateEnd = this.logger.timeStart(LogLevel.log, `Validating project${(this.logger.logLevel as LogLevel) > LogLevel.log ? ` (run ${validationRunId})` : ''}`);
2,277!
1210
                this.diagnostics.clearForTag(ProgramValidatorDiagnosticsTag);
2,277✔
1211
                this.plugins.emit('beforeValidateProgram', {
2,277✔
1212
                    program: this
1213
                });
1214
                beforeValidateProgramWasEmitted = true;
2,277✔
1215
                this.plugins.emit('validateProgram', {
2,277✔
1216
                    program: this
1217
                });
1218
            })
1219
            .once('get files to be validated', () => {
1220
                filesToProcess = Object.values(this.files).sort(firstBy(x => x.srcPath)).filter(x => !x.isValidated);
5,122✔
1221
                for (const file of filesToProcess) {
2,277✔
1222
                    filesToBeValidatedInScopeContext.add(file);
2,916✔
1223
                }
1224
            })
1225
            .once('add component reference types', () => {
1226
                // Create reference component types for any component that changes
1227
                for (let [componentKey, componentName] of this.componentSymbolsToUpdate.entries()) {
2,274✔
1228
                    this.addComponentReferenceType(componentKey, componentName);
485✔
1229
                }
1230
            })
1231
            //run before/validate/after as a single per-file action so the Sequencer can't cancel
1232
            //between them. Splitting into three forEach steps would let cancellation land after
1233
            //`validateFile` (where BrsFileValidator pushes per-node symbols via addSymbol) but
1234
            //before `afterValidateFile` (where processSymbolInformation registers validation
1235
            //segments) — leaving the file in a half-processed state. The next pass would either
1236
            //re-run BrsFileValidator (pushing duplicate symbols → CrossScopeValidator name-
1237
            //collision) or skip the file in scope validation (no segments to walk → no
1238
            //diagnostics emitted). Atomic-per-file means either the file is fully processed or
1239
            //it's untouched. Plugins that need an all-files-done signal should use
1240
            //`afterValidateProgram` instead of `afterValidateFile`.
1241
            .forEach('validateFile', () => filesToProcess, (file) => {
2,273✔
1242
                this.plugins.emit('beforeValidateFile', {
2,909✔
1243
                    program: this,
1244
                    file: file
1245
                });
1246
                this.plugins.emit('validateFile', {
2,909✔
1247
                    program: this,
1248
                    file: file
1249
                });
1250
                this.plugins.emit('afterValidateFile', {
2,909✔
1251
                    program: this,
1252
                    file: file
1253
                });
1254
                file.isValidated = true;
2,909✔
1255
                if (isBrsFile(file)) {
2,909✔
1256
                    brsFilesValidated.push(file);
2,422✔
1257
                } else if (isXmlFile(file)) {
487!
1258
                    xmlFilesValidated.push(file);
487✔
1259
                }
1260
            })
1261
            .forEach('do deferred component creation', () => [...brsFilesValidated, ...xmlFilesValidated], (file) => {
2,262✔
1262
                if (isXmlFile(file)) {
2,920✔
1263
                    this.addDeferredComponentTypeSymbolCreation(file);
487✔
1264
                } else if (isBrsFile(file) && !this.isFirstValidation) {
2,433✔
1265
                    const fileHasChanges = file.providedSymbols.changes.get(SymbolTypeFlag.runtime).size > 0 || file.providedSymbols.changes.get(SymbolTypeFlag.typetime).size > 0;
167✔
1266
                    if (fileHasChanges) {
167✔
1267
                        for (const scope of this.getScopesForFile(file)) {
108✔
1268
                            if (isXmlScope(scope) && this.doesXmlFileRequireProvidedSymbols(scope.xmlFile, file.providedSymbols.changes)) {
250✔
1269
                                this.addDeferredComponentTypeSymbolCreation(scope.xmlFile);
4✔
1270
                            }
1271
                        }
1272
                    }
1273
                }
1274
            })
1275
            .once('build component types for any component that changes', () => {
1276
                this.logger.time(LogLevel.info, ['Build component types'], () => {
2,255✔
1277
                    this.logger.debug(`Component Symbols to update:`, [...this.componentSymbolsToUpdate.entries()].sort());
2,255✔
1278
                    this.lastValidationInfo.componentsRebuilt = new Set<string>();
2,255✔
1279
                    for (let [componentKey, componentName] of this.componentSymbolsToUpdate.entries()) {
2,255✔
1280
                        this.lastValidationInfo.componentsRebuilt.add(componentName?.toLowerCase());
489✔
1281
                        if (this.updateComponentSymbolInGlobalScope(componentKey, componentName)) {
489✔
1282
                            changedComponentTypes.push(util.getSgNodeTypeName(componentName).toLowerCase());
476✔
1283
                        }
1284
                    }
1285
                    this.componentSymbolsToUpdate.clear();
2,255✔
1286
                });
1287
            })
1288
            .once('track and update type-time and runtime symbol dependencies and changes', () => {
1289
                const changedSymbolsMapArr = [...brsFilesValidated, ...xmlFilesValidated]?.map(f => {
2,253!
1290
                    if (isBrsFile(f)) {
2,909✔
1291
                        return f.providedSymbols.changes;
2,422✔
1292
                    }
1293
                    return null;
487✔
1294
                }).filter(x => x);
2,909✔
1295

1296
                // update the map of typetime dependencies
1297
                for (const file of brsFilesValidated) {
2,253✔
1298
                    for (const [symbolName, provided] of file.providedSymbols.symbolMap.get(SymbolTypeFlag.typetime).entries()) {
2,422✔
1299
                        // clear existing dependencies
1300
                        for (const values of this.symbolDependencies.values()) {
842✔
1301
                            values.delete(symbolName);
63✔
1302
                        }
1303

1304
                        // map types to the set of types that depend upon them
1305
                        for (const dependentSymbol of provided.requiredSymbolNames?.values() ?? []) {
842!
1306
                            const dependentSymbolLower = dependentSymbol.toLowerCase();
212✔
1307
                            if (!this.symbolDependencies.has(dependentSymbolLower)) {
212✔
1308
                                this.symbolDependencies.set(dependentSymbolLower, new Set<string>());
190✔
1309
                            }
1310
                            const symbolsDependentUpon = this.symbolDependencies.get(dependentSymbolLower);
212✔
1311
                            symbolsDependentUpon.add(symbolName);
212✔
1312
                        }
1313
                    }
1314
                }
1315

1316
                for (const flag of [SymbolTypeFlag.runtime, SymbolTypeFlag.typetime]) {
2,253✔
1317
                    const changedSymbolsSetArr = changedSymbolsMapArr.map(symMap => symMap.get(flag));
4,844✔
1318
                    const changedSymbolSet = new Set<string>();
4,506✔
1319
                    for (const changeSet of changedSymbolsSetArr) {
4,506✔
1320
                        for (const change of changeSet) {
4,844✔
1321
                            changedSymbolSet.add(change);
4,499✔
1322
                        }
1323
                    }
1324
                    if (!changedSymbols.has(flag)) {
4,506✔
1325
                        changedSymbols.set(flag, changedSymbolSet);
4,352✔
1326
                    } else {
1327
                        changedSymbols.set(flag, new Set([...changedSymbols.get(flag), ...changedSymbolSet]));
154✔
1328
                    }
1329
                }
1330

1331
                // update changed symbol set with any changed component
1332
                for (const changedComponentType of changedComponentTypes) {
2,253✔
1333
                    changedSymbols.get(SymbolTypeFlag.typetime).add(changedComponentType);
476✔
1334
                }
1335

1336
                // Add any additional types that depend on a changed type
1337
                // as each iteration of the loop might add new types, need to keep checking until nothing new is added
1338
                const dependentTypesChanged = new Set<string>();
2,253✔
1339
                let foundDependentTypes = false;
2,253✔
1340
                const changedTypeSymbols = changedSymbols.get(SymbolTypeFlag.typetime);
2,253✔
1341
                do {
2,253✔
1342
                    foundDependentTypes = false;
2,259✔
1343
                    const allChangedTypesSofar = [...Array.from(changedTypeSymbols), ...Array.from(dependentTypesChanged)];
2,259✔
1344
                    for (const changedSymbol of allChangedTypesSofar) {
2,259✔
1345
                        const symbolsDependentUponChangedSymbol = this.symbolDependencies.get(changedSymbol) ?? [];
1,320✔
1346
                        for (const symbolName of symbolsDependentUponChangedSymbol) {
1,320✔
1347
                            if (!changedTypeSymbols.has(symbolName) && !dependentTypesChanged.has(symbolName)) {
210✔
1348
                                foundDependentTypes = true;
6✔
1349
                                dependentTypesChanged.add(symbolName);
6✔
1350
                            }
1351
                        }
1352
                    }
1353
                } while (foundDependentTypes);
1354

1355
                changedSymbols.set(SymbolTypeFlag.typetime, new Set([...changedSymbols.get(SymbolTypeFlag.typetime), ...changedTypeSymbols, ...dependentTypesChanged]));
2,253✔
1356

1357
                this.lastValidationInfo.brsFilesSrcPath = new Set<string>(this.validationDetails.brsFilesValidated.map(f => f.srcPath?.toLowerCase() ?? ''));
2,420!
1358
                this.lastValidationInfo.xmlFilesSrcPath = new Set<string>(this.validationDetails.xmlFilesValidated.map(f => f.srcPath?.toLowerCase() ?? ''));
2,253!
1359

1360
                // can reset filesValidatedList, because they are no longer needed
1361
                this.validationDetails.brsFilesValidated = [];
2,253✔
1362
                this.validationDetails.xmlFilesValidated = [];
2,253✔
1363
            })
1364
            .once('tracks changed symbols and prepares files and scopes for validation', () => {
1365
                if (this.options.logLevel === LogLevel.debug) {
2,233!
1366
                    const changedRuntime = Array.from(changedSymbols.get(SymbolTypeFlag.runtime)).sort();
×
1367
                    this.logger.debug('Changed Symbols (runTime):', changedRuntime.join(', '));
×
1368
                    const changedTypetime = Array.from(changedSymbols.get(SymbolTypeFlag.typetime)).sort();
×
1369
                    this.logger.debug('Changed Symbols (typeTime):', changedTypetime.join(', '));
×
1370
                }
1371
                const didComponentChange = changedComponentTypes.length > 0;
2,233✔
1372
                const didProvidedSymbolChange = changedSymbols.get(SymbolTypeFlag.runtime).size > 0 || changedSymbols.get(SymbolTypeFlag.typetime).size > 0;
2,233✔
1373
                const scopesToCheck = this.getScopesForCrossScopeValidation(didComponentChange, didProvidedSymbolChange);
2,233✔
1374

1375
                this.crossScopeValidation.buildComponentsMap();
2,233✔
1376
                this.logger.time(LogLevel.info, ['addDiagnosticsForScopes'], () => {
2,233✔
1377
                    this.crossScopeValidation.addDiagnosticsForScopes(scopesToCheck);
2,233✔
1378
                });
1379
                if (!this.isFirstValidation) {
2,233✔
1380
                    const filesToRevalidate = this.crossScopeValidation.getFilesRequiringChangedSymbol(scopesToCheck, changedSymbols);
218✔
1381
                    for (const file of filesToRevalidate) {
218✔
1382
                        filesToBeValidatedInScopeContext.add(file);
174✔
1383
                    }
1384
                }
1385

1386
                this.currentScopeValidationOptions = {
2,233✔
1387
                    filesToBeValidatedInScopeContext: filesToBeValidatedInScopeContext,
1388
                    changedSymbols: changedSymbols,
1389
                    changedFiles: Array.from(filesToBeValidatedInScopeContext),
1390
                    initialValidation: this.isFirstValidation
1391
                };
1392

1393
                //can reset changedComponent types
1394
                this.validationDetails.changedComponentTypes = [];
2,233✔
1395
            })
1396
            .forEach('invalidating file segments', () => filesToBeValidatedInScopeContext, (file) => {
2,233✔
1397
                if (isBrsFile(file)) {
3,067✔
1398
                    file.validationSegmenter.unValidateAllSegments();
2,580✔
1399
                    if (!this.isFirstValidation) {
2,580✔
1400
                        scopesToInvalidate.push(...this.getScopesForFile(file));
324✔
1401
                    }
1402
                }
1403
            })
1404
            .once('invalidate affected scopes', () => {
1405
                if (this.isFirstValidation) {
2,177✔
1406
                    for (const scope of this.getAllUserScopes()) {
1,959✔
1407
                        scope.invalidate();
2,219✔
1408
                    }
1409
                } else {
1410
                    for (const scope of scopesToInvalidate) {
218✔
1411
                        scope.invalidate();
491✔
1412
                    }
1413
                }
1414
            })
1415
            .once('checking scopes to validate', () => {
1416
                //sort the scope names so we get consistent results
1417
                for (const scope of this.getAllUserScopes()) {
2,177✔
1418
                    if (scope.shouldValidate(this.currentScopeValidationOptions)) {
2,608✔
1419
                        scopesToValidate.push(scope);
2,529✔
1420
                    }
1421
                }
1422
                this.lastValidationInfo.scopeNames = new Set<string>(scopesToValidate.map(s => s.name?.toLowerCase() ?? ''));
2,537!
1423
            })
1424
            .forEach('beforeScopeValidate', () => scopesToValidate, (scope) => {
2,177✔
1425
                this.plugins.emit('beforeValidateScope', {
2,537✔
1426
                    program: this,
1427
                    scope: scope
1428
                });
1429
            })
1430
            .forEach('validate scope', () => scopesToValidate, (scope) => {
2,176✔
1431
                scope.validate(this.currentScopeValidationOptions);
2,533✔
1432
            })
1433
            .forEach('afterValidateScope', () => scopesToValidate, (scope) => {
2,171✔
1434
                this.plugins.emit('afterValidateScope', {
2,525✔
1435
                    program: this,
1436
                    scope: scope
1437
                });
1438
            })
1439
            .once('detect duplicate component names', () => {
1440
                this.detectDuplicateComponentNames();
2,170✔
1441

1442

1443
            })
1444
            .onCancel(() => {
1445
                logValidateEnd('cancelled');
121✔
1446
            })
1447
            .onSuccess(() => {
1448
                logValidateEnd();
2,170✔
1449
                this.isFirstValidation = false;
2,170✔
1450

1451
                // can reset other validation details
1452
                this.validationDetails.changedSymbols = new Map<SymbolTypeFlag, Set<string>>();
2,170✔
1453
                this.validationDetails.scopesToInvalidate = [];
2,170✔
1454
                this.validationDetails.scopesToValidate = [];
2,170✔
1455
                this.validationDetails.filesToBeValidatedInScopeContext = new Set<BscFile>();
2,170✔
1456
            })
1457
            .onComplete(() => {
1458
                //if we emitted the beforeValidateProgram hook, emit the afterValidateProgram hook as well
1459
                if (beforeValidateProgramWasEmitted) {
2,291✔
1460
                    const wasCancelled = options?.cancellationToken?.isCancellationRequested ?? false;
2,277✔
1461
                    this.plugins.emit('afterValidateProgram', {
2,277✔
1462
                        program: this,
1463
                        wasCancelled: wasCancelled
1464
                    });
1465
                }
1466

1467
                //log all the sequencer timing metrics if `info` logging is enabled
1468
                this.logger.info(
2,291✔
1469
                    sequencer.formatMetrics({
1470
                        header: 'Program.validate metrics:',
1471
                        //only include loop iterations if `debug` logging is enabled
1472
                        includeLoopIterations: this.logger.isLogLevelEnabled(LogLevel.debug)
1473
                    })
1474
                );
1475

1476
                //regardless of the success of the validation, mark this run as complete
1477
                deferred.resolve();
2,291✔
1478
                //clear the validatePromise which means we're no longer running a validation
1479
                this.validatePromise = undefined;
2,291✔
1480
            });
1481

1482
        //run the sequencer in async mode if enabled
1483
        if (options?.async) {
2,291✔
1484
            return sequencer.run();
368✔
1485

1486
            //run the sequencer in sync mode
1487
        } else {
1488
            return sequencer.runSync();
1,923✔
1489
        }
1490
    }
1491

1492
    private getScopesForCrossScopeValidation(someComponentTypeChanged: boolean, didProvidedSymbolChange: boolean) {
1493
        const scopesForCrossScopeValidation: Scope[] = [];
2,233✔
1494
        for (let scope of this.getAllUserScopes()) {
2,233✔
1495
            if (this.globalScope === scope) {
2,608!
1496
                continue;
×
1497
            }
1498
            if (someComponentTypeChanged) {
2,608✔
1499
                scopesForCrossScopeValidation.push(scope);
675✔
1500
            }
1501
            if (didProvidedSymbolChange && !scope.isValidated) {
2,608✔
1502
                scopesForCrossScopeValidation.push(scope);
2,391✔
1503
            }
1504
        }
1505
        return scopesForCrossScopeValidation;
2,233✔
1506
    }
1507

1508
    private doesXmlFileRequireProvidedSymbols(file: XmlFile, providedSymbolsByFlag: Map<SymbolTypeFlag, Set<string>>) {
1509
        for (const required of file.requiredSymbols) {
189✔
1510
            const symbolNameLower = (required as UnresolvedXMLSymbol).name.toLowerCase();
5✔
1511
            const requiredSymbolIsProvided = providedSymbolsByFlag.get(required.flags).has(symbolNameLower);
5✔
1512
            if (requiredSymbolIsProvided) {
5✔
1513
                return true;
4✔
1514
            }
1515
        }
1516
        return false;
185✔
1517
    }
1518

1519
    /**
1520
     * Flag all duplicate component names
1521
     */
1522
    private detectDuplicateComponentNames() {
1523
        const componentsByName = Object.keys(this.files).reduce<Record<string, XmlFile[]>>((map, filePath) => {
2,170✔
1524
            const file = this.files[filePath];
3,433✔
1525
            //if this is an XmlFile, and it has a valid `componentName` property
1526
            if (isXmlFile(file) && file.componentName?.text) {
3,433✔
1527
                let lowerName = file.componentName.text.toLowerCase();
695✔
1528
                if (!map[lowerName]) {
695✔
1529
                    map[lowerName] = [];
692✔
1530
                }
1531
                map[lowerName].push(file);
695✔
1532
            }
1533
            return map;
3,433✔
1534
        }, {});
1535

1536
        for (let name in componentsByName) {
2,170✔
1537
            const xmlFiles = componentsByName[name];
692✔
1538
            //add diagnostics for every duplicate component with this name
1539
            if (xmlFiles.length > 1) {
692✔
1540
                for (let xmlFile of xmlFiles) {
3✔
1541
                    const { componentName } = xmlFile;
6✔
1542
                    this.diagnostics.register({
6✔
1543
                        ...DiagnosticMessages.duplicateComponentName(componentName.text),
1544
                        location: xmlFile.componentName.location,
1545
                        relatedInformation: xmlFiles.filter(x => x !== xmlFile).map(x => {
12✔
1546
                            return {
6✔
1547
                                location: x.componentName.location,
1548
                                message: 'Also defined here'
1549
                            };
1550
                        })
1551
                    }, { tags: [ProgramValidatorDiagnosticsTag] });
1552
                }
1553
            }
1554
        }
1555
    }
1556

1557
    /**
1558
     * Get the files for a list of filePaths
1559
     * @param filePaths can be an array of srcPath or a destPath strings
1560
     * @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
1561
     */
1562
    public getFiles<T extends BscFile>(filePaths: string[], normalizePath = true) {
33✔
1563
        return filePaths
33✔
1564
            .map(filePath => this.getFile(filePath, normalizePath))
39✔
1565
            .filter(file => file !== undefined) as T[];
39✔
1566
    }
1567

1568
    private getFilePathCache = new Map<string, { path: string; isDestMap?: boolean }>();
2,663✔
1569

1570
    /**
1571
     * Get the file at the given path
1572
     * @param filePath can be a srcPath or a destPath
1573
     * @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
1574
     */
1575
    public getFile<T extends BscFile>(filePath: string, normalizePath = true) {
25,419✔
1576
        if (this.getFilePathCache.has(filePath)) {
32,674✔
1577
            const cachedFilePath = this.getFilePathCache.get(filePath);
18,375✔
1578
            if (cachedFilePath.isDestMap) {
18,375✔
1579
                return this.destMap.get(
14,422✔
1580
                    cachedFilePath.path
1581
                ) as T;
1582
            }
1583
            return this.files[
3,953✔
1584
                cachedFilePath.path
1585
            ] as T;
1586
        }
1587
        if (typeof filePath !== 'string') {
14,299✔
1588
            return undefined;
4,710✔
1589
            //is the path absolute (or the `virtual:` prefix)
1590
        } else if (/^(?:(?:virtual:[\/\\])|(?:\w:)|(?:[\/\\]))/gmi.exec(filePath)) {
9,589✔
1591
            const standardizedPath = (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase();
4,539!
1592
            this.getFilePathCache.set(filePath, { path: standardizedPath });
4,539✔
1593

1594
            return this.files[
4,539✔
1595
                standardizedPath
1596
            ] as T;
1597
        } else if (util.isUriLike(filePath)) {
5,050✔
1598
            const path = URI.parse(filePath).fsPath;
774✔
1599
            const standardizedPath = (normalizePath ? util.standardizePath(path) : path).toLowerCase();
774!
1600
            this.getFilePathCache.set(filePath, { path: standardizedPath });
774✔
1601

1602
            return this.files[
774✔
1603
                standardizedPath
1604
            ] as T;
1605
        } else {
1606
            const standardizedPath = (normalizePath ? util.standardizePath(filePath) : filePath).toLowerCase();
4,276✔
1607
            this.getFilePathCache.set(filePath, { path: standardizedPath, isDestMap: true });
4,276✔
1608
            return this.destMap.get(
4,276✔
1609
                standardizedPath
1610
            ) as T;
1611
        }
1612
    }
1613

1614
    private sortedScopeNames: string[] = undefined;
2,663✔
1615

1616
    /**
1617
     * Gets a sorted list of all scopeNames, always beginning with "global", "source", then any others in alphabetical order
1618
     */
1619
    private getSortedScopeNames() {
1620
        if (!this.sortedScopeNames) {
6,246✔
1621
            this.sortedScopeNames = Object.keys(this.scopes).sort((a, b) => {
1,894✔
1622
                if (a === 'global') {
2,617!
1623
                    return -1;
×
1624
                } else if (b === 'global') {
2,617✔
1625
                    return 1;
1,907✔
1626
                }
1627
                if (a === 'source') {
710✔
1628
                    return -1;
40✔
1629
                } else if (b === 'source') {
670✔
1630
                    return 1;
197✔
1631
                }
1632
                if (a < b) {
473✔
1633
                    return -1;
198✔
1634
                } else if (b < a) {
275!
1635
                    return 1;
275✔
1636
                }
1637
                return 0;
×
1638
            });
1639
        }
1640
        return this.sortedScopeNames;
6,246✔
1641
    }
1642

1643
    public getAllUserScopes() {
1644
        return Object.values(this.scopes).filter(s => s.name !== 'global');
13,804✔
1645
    }
1646

1647

1648
    private scopesPerFile: Map<BscFile, Scope[]> = new Map();
2,663✔
1649

1650

1651
    /**
1652
     * Get a list of all scopes the file is loaded into
1653
     * @param file the file
1654
     */
1655
    public getScopesForFile(file: BscFile | string) {
1656
        const resolvedFile = typeof file === 'string' ? this.getFile(file) : file;
1,174✔
1657

1658
        const cachedResult = this.scopesPerFile.get(resolvedFile);
1,174✔
1659
        if (cachedResult) {
1,174✔
1660
            return cachedResult;
484✔
1661
        }
1662

1663
        let result = [] as Scope[];
690✔
1664
        if (resolvedFile) {
690✔
1665
            const scopeKeys = this.getSortedScopeNames();
689✔
1666
            for (let key of scopeKeys) {
689✔
1667
                let scope = this.scopes[key];
11,737✔
1668

1669
                if (scope.hasFile(resolvedFile)) {
11,737✔
1670
                    result.push(scope);
857✔
1671
                }
1672
            }
1673
        }
1674
        this.scopesPerFile.set(resolvedFile, result);
690✔
1675
        return result;
690✔
1676
    }
1677

1678
    /**
1679
     * Get the first found scope for a file.
1680
     */
1681
    public getFirstScopeForFile(file: BscFile): Scope | undefined {
1682
        const scopeKeys = this.getSortedScopeNames();
5,557✔
1683
        for (let key of scopeKeys) {
5,557✔
1684
            let scope = this.scopes[key];
21,674✔
1685

1686
            if (scope.hasFile(file)) {
21,674✔
1687
                return scope;
4,140✔
1688
            }
1689
        }
1690
    }
1691

1692
    public getStatementsByName(name: string, originFile: BrsFile, namespaceName?: string): FileLink<Statement>[] {
1693
        let results = new Map<Statement, FileLink<Statement>>();
46✔
1694
        const filesSearched = new Set<BrsFile>();
46✔
1695
        let lowerNamespaceName = namespaceName?.toLowerCase();
46✔
1696
        let lowerName = name?.toLowerCase();
46!
1697

1698
        function addToResults(statement: FunctionStatement | MethodStatement, file: BrsFile) {
1699
            let parentNamespaceName = statement.findAncestor<NamespaceStatement>(isNamespaceStatement)?.getName(originFile.parseMode)?.toLowerCase();
111✔
1700
            if (statement.tokens.name.text.toLowerCase() === lowerName && (!lowerNamespaceName || parentNamespaceName === lowerNamespaceName)) {
111✔
1701
                if (!results.has(statement)) {
42!
1702
                    results.set(statement, { item: statement, file: file as BrsFile });
42✔
1703
                }
1704
            }
1705
        }
1706

1707
        //look through all files in scope for matches
1708
        for (const scope of this.getScopesForFile(originFile)) {
46✔
1709
            for (const file of scope.getAllFiles()) {
46✔
1710
                //skip non-brs files, or files we've already processed
1711
                if (!isBrsFile(file) || filesSearched.has(file)) {
52✔
1712
                    continue;
3✔
1713
                }
1714
                filesSearched.add(file);
49✔
1715

1716
                file.ast.walk(createVisitor({
49✔
1717
                    FunctionStatement: (statement: FunctionStatement) => {
1718
                        addToResults(statement, file);
108✔
1719
                    },
1720
                    MethodStatement: (statement: MethodStatement) => {
1721
                        addToResults(statement, file);
3✔
1722
                    }
1723
                }), {
1724
                    walkMode: WalkMode.visitStatements
1725
                });
1726
            }
1727
        }
1728
        return [...results.values()];
46✔
1729
    }
1730

1731
    public getStatementsForXmlFile(scope: XmlScope, filterName?: string): FileLink<FunctionStatement>[] {
1732
        let results = new Map<Statement, FileLink<FunctionStatement>>();
14✔
1733
        const filesSearched = new Set<BrsFile>();
14✔
1734

1735
        //get all function names for the xml file and parents
1736
        let funcNames = new Set<string>();
14✔
1737
        let currentScope = scope;
14✔
1738
        while (isXmlScope(currentScope)) {
14✔
1739
            for (let name of currentScope.xmlFile.ast.componentElement.interfaceElement?.functions.map((f) => f.name) ?? []) {
20✔
1740
                if (!filterName || name === filterName) {
20!
1741
                    funcNames.add(name);
20✔
1742
                }
1743
            }
1744
            currentScope = currentScope.getParentScope() as XmlScope;
16✔
1745
        }
1746

1747
        //look through all files in scope for matches
1748
        for (const file of scope.getOwnFiles()) {
14✔
1749
            //skip non-brs files, or files we've already processed
1750
            if (!isBrsFile(file) || filesSearched.has(file)) {
28✔
1751
                continue;
14✔
1752
            }
1753
            filesSearched.add(file);
14✔
1754

1755
            file.ast.walk(createVisitor({
14✔
1756
                FunctionStatement: (statement: FunctionStatement) => {
1757
                    if (funcNames.has(statement.tokens.name.text)) {
19!
1758
                        if (!results.has(statement)) {
19!
1759
                            results.set(statement, { item: statement, file: file });
19✔
1760
                        }
1761
                    }
1762
                }
1763
            }), {
1764
                walkMode: WalkMode.visitStatements
1765
            });
1766
        }
1767
        return [...results.values()];
14✔
1768
    }
1769

1770
    /**
1771
     * Find all available completion items at the given position
1772
     * @param filePath can be a srcPath or a destPath
1773
     * @param position the position (line & column) where completions should be found
1774
     */
1775
    public getCompletions(filePath: string, position: Position) {
1776
        let file = this.getFile(filePath);
131✔
1777
        if (!file) {
131!
1778
            return [];
×
1779
        }
1780

1781
        const event: ProvideCompletionsEvent = {
131✔
1782
            program: this,
1783
            file: file,
1784
            scopes: this.getScopesForFile(file),
1785
            position: position,
1786
            completions: []
1787
        };
1788

1789
        this.plugins.emit('beforeProvideCompletions', event);
131✔
1790

1791
        this.plugins.emit('provideCompletions', event);
131✔
1792

1793
        this.plugins.emit('afterProvideCompletions', event);
131✔
1794

1795
        return event.completions;
131✔
1796
    }
1797

1798
    /**
1799
     * Goes through each file and builds a list of workspace symbols for the program. Used by LanguageServer's onWorkspaceSymbol functionality
1800
     */
1801
    public getWorkspaceSymbols() {
1802
        const event: ProvideWorkspaceSymbolsEvent = {
22✔
1803
            program: this,
1804
            workspaceSymbols: []
1805
        };
1806
        this.plugins.emit('beforeProvideWorkspaceSymbols', event);
22✔
1807
        this.plugins.emit('provideWorkspaceSymbols', event);
22✔
1808
        this.plugins.emit('afterProvideWorkspaceSymbols', event);
22✔
1809
        return event.workspaceSymbols;
22✔
1810
    }
1811

1812
    /**
1813
     * Given a position in a file, if the position is sitting on some type of identifier,
1814
     * go to the definition of that identifier (where this thing was first defined)
1815
     */
1816
    public getDefinition(srcPath: string, position: Position): Location[] {
1817
        let file = this.getFile(srcPath);
24✔
1818
        if (!file) {
24!
1819
            return [];
×
1820
        }
1821

1822
        const event: ProvideDefinitionEvent = {
24✔
1823
            program: this,
1824
            file: file,
1825
            position: position,
1826
            definitions: []
1827
        };
1828

1829
        this.plugins.emit('beforeProvideDefinition', event);
24✔
1830
        this.plugins.emit('provideDefinition', event);
24✔
1831
        this.plugins.emit('afterProvideDefinition', event);
24✔
1832
        return event.definitions;
24✔
1833
    }
1834

1835
    /**
1836
     * Get hover information for a file and position
1837
     */
1838
    public getHover(srcPath: string, position: Position): Hover[] {
1839
        let file = this.getFile(srcPath);
97✔
1840
        let result: Hover[];
1841
        if (file) {
97!
1842
            const event = {
97✔
1843
                program: this,
1844
                file: file,
1845
                position: position,
1846
                scopes: this.getScopesForFile(file),
1847
                hovers: []
1848
            } as ProvideHoverEvent;
1849
            this.plugins.emit('beforeProvideHover', event);
97✔
1850
            this.plugins.emit('provideHover', event);
97✔
1851
            this.plugins.emit('afterProvideHover', event);
97✔
1852
            result = event.hovers;
97✔
1853
        }
1854

1855
        return result ?? [];
97!
1856
    }
1857

1858
    /**
1859
     * Get full list of document symbols for a file
1860
     * @param srcPath path to the file
1861
     */
1862
    public getDocumentSymbols(srcPath: string): DocumentSymbol[] | undefined {
1863
        let file = this.getFile(srcPath);
24✔
1864
        if (file) {
24!
1865
            const event: ProvideDocumentSymbolsEvent = {
24✔
1866
                program: this,
1867
                file: file,
1868
                documentSymbols: []
1869
            };
1870
            this.plugins.emit('beforeProvideDocumentSymbols', event);
24✔
1871
            this.plugins.emit('provideDocumentSymbols', event);
24✔
1872
            this.plugins.emit('afterProvideDocumentSymbols', event);
24✔
1873
            return event.documentSymbols;
24✔
1874
        } else {
1875
            return undefined;
×
1876
        }
1877
    }
1878

1879
    /**
1880
     * Get the selection ranges for the given positions in a file. Used for expand/shrink selection.
1881
     * @param srcPath path to the file
1882
     * @param positions the positions to get selection ranges for
1883
     */
1884
    public getSelectionRanges(srcPath: string, positions: Position[]): SelectionRange[] {
1885
        const file = this.getFile(srcPath);
15✔
1886
        if (file) {
15✔
1887
            const event: ProvideSelectionRangesEvent = {
14✔
1888
                program: this,
1889
                file: file,
1890
                positions: positions,
1891
                selectionRanges: []
1892
            };
1893
            this.plugins.emit('beforeProvideSelectionRanges', event);
14✔
1894
            this.plugins.emit('provideSelectionRanges', event);
14✔
1895
            this.plugins.emit('afterProvideSelectionRanges', event);
14✔
1896
            return event.selectionRanges;
14✔
1897
        }
1898
        return [];
1✔
1899
    }
1900

1901
    /**
1902
     * Get inlay hints for the given file and range.
1903
     */
1904
    public getInlayHints(srcPath: string, range: Range): InlayHint[] {
1905
        const file = this.getFile(srcPath);
11✔
1906
        if (file) {
11!
1907
            const event: ProvideInlayHintsEvent = {
11✔
1908
                program: this,
1909
                file: file,
1910
                range: range,
1911
                scopes: this.getScopesForFile(file),
1912
                inlayHints: []
1913
            };
1914
            this.plugins.emit('beforeProvideInlayHints', event);
11✔
1915
            this.plugins.emit('provideInlayHints', event);
11✔
1916
            this.plugins.emit('afterProvideInlayHints', event);
11✔
1917
            return event.inlayHints;
11✔
1918
        }
1919
        return [];
×
1920
    }
1921

1922
    /**
1923
     * Compute code actions for the given file and range
1924
     */
1925
    public getCodeActions(srcPath: string, range: Range) {
1926
        const codeActions = [] as CodeAction[];
72✔
1927
        const file = this.getFile(srcPath);
72✔
1928
        if (file) {
72✔
1929
            const fileUri = util.pathToUri(file?.srcPath);
71!
1930
            const diagnostics = this
71✔
1931
                //get all current diagnostics (filtered by diagnostic filters)
1932
                .getDiagnostics()
1933
                //only keep diagnostics related to this file
1934
                .filter(x => x.location?.uri === fileUri)
142!
1935
                //only keep diagnostics that touch this range
1936
                .filter(x => util.rangesIntersectOrTouch(x.location.range, range));
124✔
1937

1938
            const scopes = this.getScopesForFile(file);
71✔
1939

1940
            this.plugins.emit('beforeProvideCodeActions', {
71✔
1941
                program: this,
1942
                file: file,
1943
                range: range,
1944
                diagnostics: diagnostics,
1945
                scopes: scopes,
1946
                codeActions: codeActions
1947
            });
1948

1949
            this.plugins.emit('provideCodeActions', {
71✔
1950
                program: this,
1951
                file: file,
1952
                range: range,
1953
                diagnostics: diagnostics,
1954
                scopes: scopes,
1955
                codeActions: codeActions
1956
            });
1957

1958
            this.plugins.emit('afterProvideCodeActions', {
71✔
1959
                program: this,
1960
                file: file,
1961
                range: range,
1962
                diagnostics: diagnostics,
1963
                scopes: scopes,
1964
                codeActions: codeActions
1965
            });
1966
        }
1967
        return codeActions;
72✔
1968
    }
1969

1970
    /**
1971
     * Compute "source fix all" code actions for the given file.
1972
     * Fires the `onGetSourceFixAllCodeActions` plugin event with all diagnostics for the file (no range filter),
1973
     * then converts each contributed SourceFixAllCodeAction into an LSP CodeAction.
1974
     */
1975
    public getSourceFixAllCodeActions(srcPath: string): CodeAction[] {
1976
        const actions: SourceFixAllCodeAction[] = [];
×
1977
        const file = this.getFile(srcPath);
×
1978
        if (file) {
×
1979
            const fileUri = util.pathToUri(file.srcPath);
×
1980
            const diagnostics = this
×
1981
                .getDiagnostics()
1982
                .filter(x => x.location?.uri === fileUri);
×
1983
            const scopes = this.getScopesForFile(file);
×
1984
            this.plugins.emit('onGetSourceFixAllCodeActions', {
×
1985
                program: this,
1986
                file: file,
1987
                diagnostics: diagnostics,
1988
                scopes: scopes,
1989
                actions: actions
1990
            } as OnGetSourceFixAllCodeActionsEvent);
1991
        }
1992
        return actions.map(action => codeActionUtil.createCodeAction({
×
1993
            ...action,
1994
            kind: action.kind ?? 'source.fixAll.brighterscript' as any
×
1995
        }));
1996
    }
1997

1998
    /**
1999
     * Get semantic tokens for the specified file
2000
     */
2001
    public getSemanticTokens(srcPath: string): SemanticToken[] | undefined {
2002
        const file = this.getFile(srcPath);
26✔
2003
        if (file) {
26!
2004
            this.plugins.emit('beforeProvideSemanticTokens', {
26✔
2005
                program: this,
2006
                file: file,
2007
                scopes: this.getScopesForFile(file),
2008
                semanticTokens: undefined
2009
            });
2010
            const result = [] as SemanticToken[];
26✔
2011
            this.plugins.emit('provideSemanticTokens', {
26✔
2012
                program: this,
2013
                file: file,
2014
                scopes: this.getScopesForFile(file),
2015
                semanticTokens: result
2016
            });
2017
            this.plugins.emit('afterProvideSemanticTokens', {
26✔
2018
                program: this,
2019
                file: file,
2020
                scopes: this.getScopesForFile(file),
2021
                semanticTokens: result
2022
            });
2023
            return result;
26✔
2024
        }
2025
    }
2026

2027
    public getSignatureHelp(filePath: string, position: Position): SignatureInfoObj[] {
2028
        let file: BrsFile = this.getFile(filePath);
188✔
2029
        if (!file || !isBrsFile(file)) {
188✔
2030
            return [];
3✔
2031
        }
2032
        let callExpressionInfo = new CallExpressionInfo(file, position);
185✔
2033
        let signatureHelpUtil = new SignatureHelpUtil();
185✔
2034
        return signatureHelpUtil.getSignatureHelpItems(callExpressionInfo);
185✔
2035
    }
2036

2037
    public getReferences(srcPath: string, position: Position): Location[] {
2038
        //find the file
2039
        let file = this.getFile(srcPath);
4✔
2040

2041
        const event: ProvideReferencesEvent = {
4✔
2042
            program: this,
2043
            file: file,
2044
            position: position,
2045
            references: []
2046
        };
2047

2048
        this.plugins.emit('beforeProvideReferences', event);
4✔
2049
        this.plugins.emit('provideReferences', event);
4✔
2050
        this.plugins.emit('afterProvideReferences', event);
4✔
2051

2052
        return event.references;
4✔
2053
    }
2054

2055
    /**
2056
     * Transpile a single file and get the result as a string.
2057
     * This does not write anything to the file system.
2058
     *
2059
     * This should only be called by `LanguageServer`.
2060
     * Internal usage should call `_getTranspiledFileContents` instead.
2061
     * @param filePath can be a srcPath or a destPath
2062
     */
2063
    public async getTranspiledFileContents(filePath: string): Promise<FileTranspileResult> {
2064
        const file = this.getFile(filePath);
385✔
2065

2066
        return this.getTranspiledFileContentsPipeline.run(async () => {
385✔
2067

2068
            const result = {
385✔
2069
                destPath: file.destPath,
2070
                pkgPath: file.pkgPath,
2071
                srcPath: file.srcPath
2072
            } as FileTranspileResult;
2073

2074
            const expectedPkgPath = file.pkgPath.toLowerCase();
385✔
2075
            const expectedMapPath = `${expectedPkgPath}.map`;
385✔
2076
            const expectedTypedefPkgPath = expectedPkgPath.replace(/\.brs$/i, '.d.bs');
385✔
2077

2078
            //add a temporary plugin to tap into the file writing process
2079
            const plugin = this.plugins.addFirst({
385✔
2080
                name: 'getTranspiledFileContents',
2081
                beforeWriteFile: (event) => {
2082
                    const pkgPath = event.file.pkgPath.toLowerCase();
1,248✔
2083
                    switch (pkgPath) {
1,248✔
2084
                        //this is the actual transpiled file
2085
                        case expectedPkgPath:
1,248✔
2086
                            result.code = event.file.data.toString();
385✔
2087
                            break;
385✔
2088
                        //this is the sourcemap
2089
                        case expectedMapPath:
2090
                            result.map = event.file.data.toString();
229✔
2091
                            break;
229✔
2092
                        //this is the typedef
2093
                        case expectedTypedefPkgPath:
2094
                            result.typedef = event.file.data.toString();
10✔
2095
                            break;
10✔
2096
                        default:
2097
                        //no idea what this file is. just ignore it
2098
                    }
2099
                    //mark every file as processed so it they don't get written to the output directory
2100
                    event.processedFiles.add(event.file);
1,248✔
2101
                }
2102
            });
2103

2104
            try {
385✔
2105
                //now that the plugin has been registered, run the build with just this file
2106
                await this.build({
385✔
2107
                    files: [file]
2108
                });
2109
            } finally {
2110
                this.plugins.remove(plugin);
385✔
2111
            }
2112
            return result;
385✔
2113
        });
2114
    }
2115
    private getTranspiledFileContentsPipeline = new ActionPipeline();
2,663✔
2116

2117
    /**
2118
     * Get the absolute output path for a file
2119
     */
2120
    private getOutputPath(file: { pkgPath?: string }, outDir = this.getOutDir()) {
×
2121
        return s`${outDir}/${file.pkgPath}`;
2,386✔
2122
    }
2123

2124
    private getOutDir(outDir?: string) {
2125
        let result = outDir ?? this.options.outDir ?? this.options.outDir;
911!
2126
        if (!result) {
911!
2127
            result = rokuDeploy.getOptions(this.options as any).outDir;
×
2128
        }
2129
        result = s`${path.resolve(this.options.cwd ?? process.cwd(), result ?? '/')}`;
911!
2130
        return result;
911✔
2131
    }
2132

2133
    /**
2134
     * Prepare the program for building
2135
     * @param files the list of files that should be prepared
2136
     */
2137
    private async prepare(files: BscFile[]) {
2138
        const programEvent: PrepareProgramEvent = {
456✔
2139
            program: this,
2140
            editor: this.editor,
2141
            files: files
2142
        };
2143

2144
        //assign an editor to every file
2145
        for (const file of programEvent.files) {
456✔
2146
            //if the file doesn't have an editor yet, assign one now
2147
            if (!file.editor) {
920✔
2148
                file.editor = new Editor();
873✔
2149
            }
2150
        }
2151

2152
        //sort the entries to make transpiling more deterministic
2153
        programEvent.files.sort((a, b) => {
456✔
2154
            if (a.pkgPath < b.pkgPath) {
478✔
2155
                return -1;
412✔
2156
            } else if (a.pkgPath > b.pkgPath) {
66!
2157
                return 1;
66✔
2158
            } else {
2159
                return 1;
×
2160
            }
2161
        });
2162

2163
        await this.plugins.emitAsync('beforePrepareProgram', programEvent);
456✔
2164
        await this.plugins.emitAsync('prepareProgram', programEvent);
456✔
2165

2166
        const outDir = this.getOutDir();
456✔
2167

2168
        const entries: TranspileObj[] = [];
456✔
2169

2170
        for (const file of files) {
456✔
2171
            const scope = this.getFirstScopeForFile(file);
920✔
2172
            //link the symbol table for all the files in this scope
2173
            scope?.linkSymbolTable();
920✔
2174

2175
            //if the file doesn't have an editor yet, assign one now
2176
            if (!file.editor) {
920!
2177
                file.editor = new Editor();
×
2178
            }
2179
            const event = {
920✔
2180
                program: this,
2181
                file: file,
2182
                editor: file.editor,
2183
                scope: scope,
2184
                outputPath: this.getOutputPath(file, outDir)
2185
            } as PrepareFileEvent & { outputPath: string };
2186

2187
            await this.plugins.emitAsync('beforePrepareFile', event);
920✔
2188
            await this.plugins.emitAsync('prepareFile', event);
920✔
2189
            await this.plugins.emitAsync('afterPrepareFile', event);
920✔
2190

2191
            //TODO remove this in v1
2192
            entries.push(event);
920✔
2193

2194
            //unlink the symbolTable so the next loop iteration can link theirs
2195
            scope?.unlinkSymbolTable();
920✔
2196
        }
2197

2198
        await this.plugins.emitAsync('afterPrepareProgram', programEvent);
456✔
2199
        return files;
456✔
2200
    }
2201

2202
    /**
2203
     * Generate the contents of every file
2204
     */
2205
    private async serialize(files: BscFile[]) {
2206

2207
        const allFiles = new Map<BscFile, SerializedFile[]>();
455✔
2208

2209
        //exclude prunable files if that option is enabled
2210
        if (this.options.pruneEmptyCodeFiles === true) {
455✔
2211
            files = files.filter(x => x.canBePruned !== true);
9✔
2212
        }
2213

2214
        const serializeProgramEvent = await this.plugins.emitAsync('beforeSerializeProgram', {
455✔
2215
            program: this,
2216
            files: files,
2217
            result: allFiles
2218
        });
2219
        await this.plugins.emitAsync('serializeProgram', serializeProgramEvent);
455✔
2220

2221
        // serialize each file
2222
        for (const file of files) {
455✔
2223
            let scope = this.getFirstScopeForFile(file);
917✔
2224

2225
            //if the file doesn't have a scope, create a temporary scope for the file so it can depend on scope-level items
2226
            if (!scope) {
917✔
2227
                scope = new Scope(`temporary-for-${file.pkgPath}`, this);
467✔
2228
                scope.getAllFiles = () => [file];
2,339✔
2229
                scope.getOwnFiles = scope.getAllFiles;
467✔
2230
            }
2231

2232
            //link the symbol table for all the files in this scope
2233
            scope?.linkSymbolTable();
917!
2234
            const event: SerializeFileEvent = {
917✔
2235
                program: this,
2236
                file: file,
2237
                scope: scope,
2238
                result: allFiles
2239
            };
2240
            await this.plugins.emitAsync('beforeSerializeFile', event);
917✔
2241
            await this.plugins.emitAsync('serializeFile', event);
917✔
2242
            await this.plugins.emitAsync('afterSerializeFile', event);
917✔
2243
            //unlink the symbolTable so the next loop iteration can link theirs
2244
            scope?.unlinkSymbolTable();
917!
2245
        }
2246

2247
        this.plugins.emit('afterSerializeProgram', serializeProgramEvent);
455✔
2248

2249
        return allFiles;
455✔
2250
    }
2251

2252
    /**
2253
     * Write the entire project to disk
2254
     */
2255
    private async write(outDir: string, files: Map<BscFile, SerializedFile[]>) {
2256
        const programEvent = await this.plugins.emitAsync('beforeWriteProgram', {
455✔
2257
            program: this,
2258
            files: files,
2259
            outDir: outDir
2260
        });
2261
        //empty the out directory
2262
        await fsExtra.emptyDir(outDir);
455✔
2263

2264
        const serializedFiles = [...files]
455✔
2265
            .map(([, serializedFiles]) => serializedFiles)
917✔
2266
            .flat();
2267

2268
        //write all the files to disk (asynchronously)
2269
        await Promise.all(
455✔
2270
            serializedFiles.map(async (file) => {
2271
                const event = await this.plugins.emitAsync('beforeWriteFile', {
1,466✔
2272
                    program: this,
2273
                    file: file,
2274
                    outputPath: this.getOutputPath(file, outDir),
2275
                    processedFiles: new Set<SerializedFile>()
2276
                });
2277

2278
                await this.plugins.emitAsync('writeFile', event);
1,466✔
2279

2280
                await this.plugins.emitAsync('afterWriteFile', event);
1,466✔
2281
            })
2282
        );
2283

2284
        await this.plugins.emitAsync('afterWriteProgram', programEvent);
455✔
2285
    }
2286

2287
    private buildPipeline = new ActionPipeline();
2,663✔
2288

2289
    /**
2290
     * Build the project. This transpiles/transforms/copies all files and moves them to the staging directory
2291
     * @param options the list of options used to build the program
2292
     */
2293
    public async build(options?: ProgramBuildOptions) {
2294
        //run a single build at a time
2295
        await this.buildPipeline.run(async () => {
455✔
2296
            const outDir = this.getOutDir(options?.outDir);
455✔
2297

2298
            const event = await this.plugins.emitAsync('beforeBuildProgram', {
455✔
2299
                program: this,
2300
                editor: this.editor,
2301
                files: options?.files ?? Object.values(this.files)
2,730✔
2302
            });
2303

2304
            //prepare the program (and files) for building
2305
            event.files = await this.prepare(event.files);
455✔
2306

2307
            //stage the entire program
2308
            const serializedFilesByFile = await this.serialize(event.files);
455✔
2309

2310
            await this.write(outDir, serializedFilesByFile);
455✔
2311

2312
            await this.plugins.emitAsync('afterBuildProgram', event);
455✔
2313

2314
            //undo all edits for the program
2315
            this.editor.undoAll();
455✔
2316
            //undo all edits for each file
2317
            for (const file of event.files) {
455✔
2318
                file.editor.undoAll();
918✔
2319
            }
2320
        });
2321

2322
        this.logger.debug('Types Created', TypesCreated);
455✔
2323
        let totalTypesCreated = 0;
455✔
2324
        for (const key in TypesCreated) {
455✔
2325
            if (TypesCreated.hasOwnProperty(key)) {
14,552!
2326
                totalTypesCreated += TypesCreated[key];
14,552✔
2327

2328
            }
2329
        }
2330
        this.logger.info('Total Types Created', totalTypesCreated);
455✔
2331
    }
2332

2333

2334
    /**
2335
     * Find a list of files in the program that have a function with the given name (case INsensitive)
2336
     */
2337
    public findFilesForFunction(functionName: string) {
2338
        const files = [] as BscFile[];
47✔
2339
        const lowerFunctionName = functionName.toLowerCase();
47✔
2340
        //find every file with this function defined
2341
        for (const file of Object.values(this.files)) {
47✔
2342
            if (isBrsFile(file)) {
137✔
2343
                //TODO handle namespace-relative function calls
2344
                //if the file has a function with this name
2345
                // eslint-disable-next-line @typescript-eslint/dot-notation
2346
                if (file['_cachedLookups'].functionStatementMap.get(lowerFunctionName)) {
102✔
2347
                    files.push(file);
24✔
2348
                }
2349
            }
2350
        }
2351
        return files;
47✔
2352
    }
2353

2354
    /**
2355
     * Find a list of files in the program that have a class with the given name (case INsensitive)
2356
     */
2357
    public findFilesForClass(className: string) {
2358
        const files = [] as BscFile[];
47✔
2359
        const lowerClassName = className.toLowerCase();
47✔
2360
        //find every file with this class defined
2361
        for (const file of Object.values(this.files)) {
47✔
2362
            if (isBrsFile(file)) {
137✔
2363
                //TODO handle namespace-relative classes
2364
                //if the file has a function with this name
2365

2366
                // eslint-disable-next-line @typescript-eslint/dot-notation
2367
                if (file['_cachedLookups'].classStatementMap.get(lowerClassName) !== undefined) {
102✔
2368
                    files.push(file);
3✔
2369
                }
2370
            }
2371
        }
2372
        return files;
47✔
2373
    }
2374

2375
    public findFilesForNamespace(name: string) {
2376
        const files = [] as BscFile[];
47✔
2377
        const lowerName = name.toLowerCase();
47✔
2378
        //find every file with this class defined
2379
        for (const file of Object.values(this.files)) {
47✔
2380
            if (isBrsFile(file)) {
137✔
2381

2382
                // eslint-disable-next-line @typescript-eslint/dot-notation
2383
                if (file['_cachedLookups'].namespaceStatements.find((x) => {
102✔
2384
                    const namespaceName = x.name.toLowerCase();
14✔
2385
                    return (
14✔
2386
                        //the namespace name matches exactly
2387
                        namespaceName === lowerName ||
18✔
2388
                        //the full namespace starts with the name (honoring the part boundary)
2389
                        namespaceName.startsWith(lowerName + '.')
2390
                    );
2391
                })) {
2392
                    files.push(file);
12✔
2393
                }
2394
            }
2395
        }
2396

2397
        return files;
47✔
2398
    }
2399

2400
    public findFilesForEnum(name: string) {
2401
        const files = [] as BscFile[];
48✔
2402
        const lowerName = name.toLowerCase();
48✔
2403
        //find every file with this enum defined
2404
        for (const file of Object.values(this.files)) {
48✔
2405
            if (isBrsFile(file)) {
138✔
2406
                // eslint-disable-next-line @typescript-eslint/dot-notation
2407
                if (file['_cachedLookups'].enumStatementMap.get(lowerName)) {
103✔
2408
                    files.push(file);
1✔
2409
                }
2410
            }
2411
        }
2412
        return files;
48✔
2413
    }
2414

2415
    private _manifest: Map<string, string>;
2416
    private _manifestEntries: ManifestEntry[];
2417

2418
    /**
2419
     * The absolute source path to the manifest file. Set when loadManifest is called.
2420
     */
2421
    public manifestPath: string;
2422

2423
    /**
2424
     * Modify a parsed manifest map by reading `bs_const` and injecting values from `options.manifest.bs_const`
2425
     * @param parsedManifest The manifest map to read from and modify
2426
     */
2427
    private buildBsConstsIntoParsedManifest(parsedManifest: Map<string, string>) {
2428
        // Lift the bs_consts defined in the manifest
2429
        let bsConsts = getBsConst(parsedManifest, false);
547✔
2430

2431
        // Override or delete any bs_consts defined in the bs config
2432
        for (const key in this.options?.manifest?.bs_const) {
547!
2433
            const value = this.options.manifest.bs_const[key];
3✔
2434
            if (value === null) {
3✔
2435
                bsConsts.delete(key);
1✔
2436
            } else {
2437
                bsConsts.set(key, value);
2✔
2438
            }
2439
        }
2440

2441
        // convert the new list of bs consts back into a string for the rest of the down stream systems to use
2442
        let constString = '';
547✔
2443
        for (const [key, value] of bsConsts) {
547✔
2444
            constString += `${constString !== '' ? ';' : ''}${key}=${value.toString()}`;
494✔
2445
        }
2446

2447
        // Set the updated bs_const value
2448
        parsedManifest.set('bs_const', constString);
547✔
2449
    }
2450

2451
    /**
2452
     * Try to find and load the manifest into memory
2453
     * @param manifestFileObj A pointer to a potential manifest file object found during loading
2454
     * @param replaceIfAlreadyLoaded should we overwrite the internal `_manifest` if it already exists
2455
     */
2456
    public loadManifest(manifestFileObj?: FileObj, replaceIfAlreadyLoaded = true) {
2,267✔
2457
        //if we already have a manifest instance, and should not replace...then don't replace
2458
        if (!replaceIfAlreadyLoaded && this._manifest) {
2,295!
2459
            return;
×
2460
        }
2461
        let manifestPath = manifestFileObj
2,295✔
2462
            ? manifestFileObj.src
2,295✔
2463
            : path.join(this.options.rootDir, 'manifest');
2464

2465
        //store the resolved manifest path so it can be used externally for change detection
2466
        this.manifestPath = util.standardizePath(manifestPath);
2,295✔
2467

2468
        try {
2,295✔
2469
            // we only load this manifest once, so do it sync to improve speed downstream
2470
            const contents = fsExtra.readFileSync(manifestPath, 'utf-8');
2,295✔
2471
            const parsedManifest = parseManifest(contents);
547✔
2472
            this.buildBsConstsIntoParsedManifest(parsedManifest);
547✔
2473
            this._manifest = parsedManifest;
547✔
2474
            this._manifestEntries = parseManifestEntries(contents);
547✔
2475
            this._manifestPath = manifestPath;
547✔
2476
        } catch (e) {
2477
            this._manifest = new Map();
1,748✔
2478
            this._manifestEntries = [];
1,748✔
2479
            this._manifestPath = undefined;
1,748✔
2480
        }
2481
    }
2482

2483
    /**
2484
     * Get a map of the manifest information
2485
     */
2486
    public getManifest() {
2487
        if (!this._manifest) {
3,309✔
2488
            this.loadManifest();
2,096✔
2489
        }
2490
        return this._manifest;
3,309✔
2491
    }
2492

2493
    /**
2494
     * Get the manifest as a list of `{ key, value, range }` entries with line/column ranges
2495
     * suitable for attaching diagnostics to specific manifest lines.
2496
     *
2497
     * NOTE: protected for now. The shape of this data is likely to evolve as we build out
2498
     * more manifest-aware features (validation rules, autocomplete, etc.). External plugins
2499
     * shouldn't depend on this until we commit to a stable API.
2500
     */
2501
    protected getManifestEntries(): ManifestEntry[] {
2502
        if (!this._manifestEntries) {
2,278✔
2503
            this.loadManifest();
170✔
2504
        }
2505
        return this._manifestEntries;
2,278✔
2506
    }
2507

2508
    private _manifestPath: string | undefined;
2509

2510
    /**
2511
     * Returns the absolute path of the loaded manifest file, or undefined if no manifest was found.
2512
     *
2513
     * NOTE: protected for now. Once brighterscript treats the manifest as a proper file
2514
     * (with editor / BscFile integration) callers should consume that instead of poking at
2515
     * the Program. External plugins shouldn't depend on this in the meantime.
2516
     */
2517
    protected getManifestPath(): string | undefined {
2518
        if (!this._manifest) {
2,276!
2519
            this.loadManifest();
×
2520
        }
2521
        return this._manifestPath;
2,276✔
2522
    }
2523

2524
    private _minFirmwareVersion: string | undefined;
2525

2526
    /**
2527
     * The minimum Roku firmware version brighterscript should assume the user is targeting.
2528
     * If `options.minFirmwareVersion` is set AND parseable as semver, the canonical coerced
2529
     * form ("15.0" → "15.0.0") wins; otherwise (unset or unparseable) falls back to
2530
     * {@link DEFAULT_MIN_FIRMWARE_VERSION}. The return is always a valid semver string so
2531
     * downstream callers can pass it directly to `semver.gte`/`semver.lt` without re-coercing.
2532
     * Cached after first call.
2533
     */
2534
    public getMinFirmwareVersion(): string {
2535
        if (this._minFirmwareVersion === undefined) {
70✔
2536
            const userValue = this.options.minFirmwareVersion;
48✔
2537
            const coerced = userValue ? semver.coerce(userValue) : undefined;
48✔
2538
            this._minFirmwareVersion = coerced ? coerced.version : DEFAULT_MIN_FIRMWARE_VERSION;
48✔
2539
        }
2540
        return this._minFirmwareVersion;
70✔
2541
    }
2542

2543
    private _rsgVersion: string | undefined;
2544

2545
    /**
2546
     * Returns the effective `rsg_version` for this program in canonical semver form (e.g. "1.2"
2547
     * → "1.2.0"). If the manifest declares a value explicitly and it's parseable, the canonical
2548
     * form wins; otherwise (manifest silent OR value is malformed) we fall back to the highest
2549
     * known rsg_version whose `becameDefaultAt` is `<=` the effective minimum firmware version,
2550
     * driven by {@link RSG_VERSIONS}. Cached after first call.
2551
     *
2552
     * Manifest validation (format errors, etc.) happens in `ProgramValidator` against the raw
2553
     * entry — that path doesn't go through this method.
2554
     */
2555
    public getRsgVersion(): string {
2556
        if (this._rsgVersion !== undefined) {
16!
2557
            return this._rsgVersion;
×
2558
        }
2559
        const explicit = this.getManifest().get('rsg_version');
16✔
2560
        const explicitCoerced = explicit !== undefined ? semver.coerce(explicit.trim()) : undefined;
16✔
2561
        if (explicitCoerced) {
16✔
2562
            this._rsgVersion = explicitCoerced.version;
5✔
2563
            return this._rsgVersion;
5✔
2564
        }
2565
        //walk known rsg_versions in descending order (newest first) and return the first whose
2566
        //becameDefaultAt <= effective firmware. As long as some entry has becameDefaultAt: '0.0.0'
2567
        //(currently `1.1`), this loop always finds a match.
2568
        const minFirmwareVersion = this.getMinFirmwareVersion();
11✔
2569
        const candidates = Object.entries(RSG_VERSIONS)
11✔
2570
            .filter(([, info]) => info.becameDefaultAt !== undefined)
44✔
2571
            .sort(([a], [b]) => semver.rcompare(semver.coerce(a)!, semver.coerce(b)!));
33✔
2572
        for (const [version, info] of candidates) {
11✔
2573
            if (semver.gte(minFirmwareVersion, info.becameDefaultAt!)) {
25✔
2574
                this._rsgVersion = semver.coerce(version)!.version;
11✔
2575
                return this._rsgVersion;
11✔
2576
            }
2577
        }
2578
        //unreachable as long as RSG_VERSIONS contains an entry with becameDefaultAt: '0.0.0'
2579
        this._rsgVersion = DEFAULT_MIN_FIRMWARE_VERSION;
×
2580
        return this._rsgVersion;
×
2581
    }
2582

2583
    public dispose() {
2584
        this.plugins.emit('beforeRemoveProgram', { program: this });
2,448✔
2585

2586
        for (let filePath in this.files) {
2,448✔
2587
            this.files[filePath]?.dispose?.();
3,012!
2588
        }
2589
        for (let name in this.scopes) {
2,448✔
2590
            this.scopes[name]?.dispose?.();
5,069!
2591
        }
2592
        this.globalScope?.dispose?.();
2,448!
2593
        this.dependencyGraph?.dispose?.();
2,448!
2594
        this.plugins.emit('removeProgram', { program: this });
2,448✔
2595
        this.plugins.emit('afterRemoveProgram', { program: this });
2,448✔
2596
    }
2597
}
2598

2599
export interface FileTranspileResult {
2600
    srcPath: string;
2601
    destPath: string;
2602
    pkgPath: string;
2603
    code: string;
2604
    map: string;
2605
    typedef: string;
2606
}
2607

2608

2609
class ProvideFileEventInternal<TFile extends BscFile = BscFile> implements ProvideFileEvent<TFile> {
2610
    constructor(
2611
        public program: Program,
3,424✔
2612
        public srcPath: string,
3,424✔
2613
        public destPath: string,
3,424✔
2614
        public data: LazyFileData,
3,424✔
2615
        public fileFactory: FileFactory
3,424✔
2616
    ) {
2617
        this.srcExtension = path.extname(srcPath)?.toLowerCase();
3,424!
2618
    }
2619

2620
    public srcExtension: string;
2621

2622
    public files: TFile[] = [];
3,424✔
2623
}
2624

2625
export interface ProgramBuildOptions {
2626
    /**
2627
     * The directory where the final built files should be placed. This directory will be cleared before running
2628
     */
2629
    outDir?: string;
2630
    /**
2631
     * An array of files to build. If omitted, the entire list of files from the program will be used instead.
2632
     * Typically you will want to leave this blank
2633
     */
2634
    files?: BscFile[];
2635
}
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