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

rokucommunity / brighterscript / #15048

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

push

web-flow
Merge 02ba2bb57 into 2ea4d2108

14498 of 17595 branches covered (82.4%)

Branch coverage included in aggregate %.

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

897 existing lines in 48 files now uncovered.

15248 of 16577 relevant lines covered (91.98%)

24112.76 hits per line

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

86.98
/src/files/XmlFile.ts
1
import * as path from 'path';
1✔
2
import type { CodeWithSourceMap } from 'source-map';
3
import { SourceNode } from 'source-map';
1✔
4
import type { Location, Position, Range } from 'vscode-languageserver';
5
import { DiagnosticCodeMap, DiagnosticLegacyCodeMap, diagnosticCodes } from '../DiagnosticMessages';
1✔
6
import type { Callable, FileReference, CommentFlag, SerializedCodeFile } from '../interfaces';
7
import type { Program } from '../Program';
8
import util from '../util';
1✔
9
import { standardizePath as s } from '../util';
1✔
10
import SGParser from '../parser/SGParser';
1✔
11
import chalk from 'chalk';
1✔
12
import { Cache } from '../Cache';
1✔
13
import type { DependencyChangedEvent, DependencyGraph } from '../DependencyGraph';
14
import type { SGInterfaceField, SGInterfaceFunction, SGToken } from '../parser/SGTypes';
15
import { CommentFlagProcessor } from '../CommentFlagProcessor';
1✔
16
import type { IToken, TokenType } from 'chevrotain';
17
import { TranspileState } from '../parser/TranspileState';
1✔
18
import type { BscFile } from './BscFile';
19
import type { Editor } from '../astUtils/Editor';
20
import type { FunctionScope } from '../FunctionScope';
21
import { SymbolTypeFlag } from '../SymbolTypeFlag';
1✔
22

23

24
export interface UnresolvedXMLSymbol {
25
    flags: SymbolTypeFlag;
26
    name: string;
27
    file: XmlFile;
28
}
29

30

31
export class XmlFile implements BscFile {
1✔
32
    /**
33
     * Create a new instance of BrsFile
34
     */
35
    constructor(options: {
36
        /**
37
         * The absolute path to the source file on disk (e.g. '/usr/you/projects/RokuApp/source/main.brs' or 'c:/projects/RokuApp/source/main.brs').
38
         */
39
        srcPath: string;
40
        /**
41
         * The absolute path to the file on-device (i.e. 'source/main.brs') without the leading `pkg:/`
42
         */
43
        destPath: string;
44
        pkgPath?: string;
45
        program: Program;
46
    }) {
47
        if (options) {
535!
48
            this.srcPath = s`${options.srcPath}`;
535✔
49
            this.destPath = s`${options.destPath}`;
535✔
50
            this.pkgPath = s`${options.pkgPath ?? options.destPath}`;
535!
51
            this.program = options.program;
535✔
52

53
            this.extension = path.extname(this.srcPath).toLowerCase();
535✔
54

55
            this.possibleCodebehindDestPaths = [
535✔
56
                this.pkgPath.replace(/\.xml$/, '.bs'),
57
                this.pkgPath.replace(/\.xml$/, '.brs')
58
            ];
59
        }
60
    }
61

62
    public type = 'XmlFile';
535✔
63

64
    /**
65
     * The absolute path to the source file on disk (e.g. '/usr/you/projects/RokuApp/source/main.brs' or 'c:/projects/RokuApp/source/main.brs').
66
     */
67
    public srcPath: string;
68
    /**
69
     * The absolute path to the file on-device (i.e. 'source/main.brs') without the leading `pkg:/`
70
     */
71
    public destPath: string;
72
    public pkgPath: string;
73

74
    public program: Program;
75

76
    /**
77
     * An editor assigned during the build flow that manages edits that will be undone once the build process is complete.
78
     */
79
    public editor?: Editor;
80

81
    /**
82
     * The absolute path to the source location for this file
83
     * @deprecated use `srcPath` instead
84
     */
85
    public get pathAbsolute() {
UNCOV
86
        return this.srcPath;
×
87
    }
88
    public set pathAbsolute(value) {
UNCOV
89
        this.srcPath = value;
×
90
    }
91

92
    private cache = new Cache();
535✔
93

94
    /**
95
     * The list of possible autoImport codebehind pkg paths.
96
     * @deprecated use `possibleCodebehindDestPaths` instead.
97
     */
98
    public get possibleCodebehindPkgPaths() {
UNCOV
99
        return this.possibleCodebehindDestPaths;
×
100
    }
101
    public set possibleCodebehindPkgPaths(value) {
UNCOV
102
        this.possibleCodebehindDestPaths = value;
×
103
    }
104

105
    /**
106
     * The list of possible autoImport codebehind destPath values
107
     */
108
    public possibleCodebehindDestPaths: string[];
109

110
    /**
111
     * An unsubscribe function for the dependencyGraph subscription
112
     */
113
    private unsubscribeFromDependencyGraph: () => void;
114

115
    /**
116
     * Indicates whether this file needs to be validated.
117
     * Files are only ever validated a single time
118
     */
119
    public isValidated = false;
535✔
120

121
    /**
122
     * The extension for this file
123
     */
124
    public extension: string;
125

126
    public commentFlags = [] as CommentFlag[];
535✔
127

128
    /**
129
     * Will this file result in only comment or whitespace output? If so, it can be excluded from the output if that bsconfig setting is enabled.
130
     */
131
    readonly canBePruned = false;
535✔
132

133
    /**
134
     * The list of script imports delcared in the XML of this file.
135
     * This excludes parent imports and auto codebehind imports
136
     */
137
    public get scriptTagImports(): FileReference[] {
138
        return this.parser.references.scriptTagImports
1,581✔
139
            .map(tag => ({
931✔
140
                ...tag,
141
                sourceFile: this
142
            }));
143
    }
144

145
    /**
146
     * List of all `destPath` values pointing to scripts that this XmlFile depends on, regardless of whether they are loaded in the program or not.
147
     * This includes own dependencies and all parent compoent dependencies
148
     * coming from:
149
     *  - script tags
150
     *  - implied codebehind file
151
     *  - import statements from imported scripts or their descendents
152
     */
153
    public getAllDependencies() {
UNCOV
154
        return this.cache.getOrAdd(`allScriptImports`, () => {
×
UNCOV
155
            const value = this.dependencyGraph.getAllDependencies(this.dependencyGraphKey);
×
UNCOV
156
            return value;
×
157
        });
158
    }
159

160
    /**
161
     * List of all destPaths to scripts that this XmlFile depends on directly, regardless of whether they are loaded in the program or not.
162
     * This does not account for parent component scripts
163
     * coming from:
164
     *  - script tags
165
     *  - implied codebehind file
166
     *  - import statements from imported scripts or their descendents
167
     */
168
    public getOwnDependencies() {
169
        return this.cache.getOrAdd(`ownScriptImports`, () => {
1,360✔
170
            const value = this.dependencyGraph.getAllDependencies(this.dependencyGraphKey, [this.parentComponentDependencyGraphKey]);
574✔
171
            return value;
574✔
172
        });
173
    }
174

175
    /**
176
     * List of all destPaths to scripts that this XmlFile depends on that are actually loaded into the program.
177
     * This does not account for parent component scripts.
178
     * coming from:
179
     *  - script tags
180
     *  - inferred codebehind file
181
     *  - import statements from imported scripts or their descendants
182
     */
183
    public getAvailableScriptImports() {
184
        return this.cache.getOrAdd('allAvailableScriptImports', () => {
29✔
185

186
            let allDependencies = this.getOwnDependencies()
28✔
187
                //skip typedef files
188
                .filter(x => util.getExtension(x) !== '.d.bs');
49✔
189

190
            let result = [] as string[];
28✔
191
            let filesInProgram = this.program.getFiles(allDependencies);
28✔
192
            for (let file of filesInProgram) {
28✔
193
                result.push(file.destPath);
32✔
194
            }
195
            this.logDebug('computed allAvailableScriptImports', () => result);
28✔
196
            return result;
28✔
197
        });
198
    }
199

200
    public get requiredSymbols() {
201
        return this.cache.getOrAdd(`requiredSymbols`, () => {
585✔
202
            this.program.logger.debug('Getting required symbols', this.srcPath);
415✔
203

204

205
            const requiredSymbols: UnresolvedXMLSymbol[] = [];
415✔
206

207
            const allInterfaceFunctions = this.parser.ast.componentElement?.interfaceElement?.getElementsByTagName<SGInterfaceFunction>('function') ?? [];
415!
208

209
            for (const node of allInterfaceFunctions) {
415✔
210
                if (node.name) {
80✔
211
                    requiredSymbols.push({
77✔
212
                        flags: SymbolTypeFlag.runtime,
213
                        file: this,
214
                        name: node.name.toLowerCase()
215
                    });
216
                }
217
            }
218

219
            const allInterfaceFields = this.parser.ast.componentElement?.interfaceElement?.getElementsByTagName<SGInterfaceField>('field') ?? [];
415!
220

221
            for (const node of allInterfaceFields) {
415✔
222
                if (node.onChange) {
16✔
223
                    requiredSymbols.push({
1✔
224
                        flags: SymbolTypeFlag.runtime,
225
                        file: this,
226
                        name: node.onChange.toLowerCase()
227
                    });
228
                }
229
                // TODO: when we can specify proper types in fields, add those types too:
230
                //if (node.type && isCustomXmlType(node.type)) {
231
                //    requiredSymbols.push({
232
                //        flags: SymbolTypeFlag.typetime,
233
                //        file: this,
234
                //        name: node.type.toLowerCase()
235
                //    });
236
                //}
237
            }
238
            return requiredSymbols;
415✔
239
        });
240
    }
241

242

243
    /**
244
     * The range of the entire file
245
     */
246
    public fileRange: Range;
247

248
    public parser = new SGParser();
535✔
249

250
    //TODO implement the xml CDATA parsing, which would populate this list
251
    public callables = [] as Callable[];
535✔
252

253
    public functionScopes = [] as FunctionScope[];
535✔
254

255
    /**
256
     * The name of the component that this component extends.
257
     * Available after `parse()`
258
     */
259
    public get parentComponentName(): SGToken {
260
        return this.parser?.references.extends;
5,136!
261
    }
262

263
    /**
264
     * The name of the component declared in this xml file
265
     * Available after `parse()`
266
     */
267
    public get componentName(): SGToken {
268
        return this.parser?.references.name;
10,591!
269
    }
270

271
    /**
272
     * Does this file need to be transpiled?
273
     * @deprecated use the `.editor` property to push changes to the file, which will force transpilation
274
     */
275
    public get needsTranspiled() {
276
        if (this._needsTranspiled !== undefined) {
34✔
277
            return this._needsTranspiled;
2✔
278
        }
279
        return !!(
32✔
280
            this.editor?.hasChanges || this.ast.componentElement?.scriptElements?.some(
170!
281
                script => script.type?.indexOf('brighterscript') > 0 || script.uri?.endsWith('.bs')
4!
282
            )
283
        );
284
    }
285
    public set needsTranspiled(value) {
286
        this._needsTranspiled = value;
2✔
287
    }
288
    public _needsTranspiled: boolean;
289

290
    /**
291
     * The AST for this file
292
     */
293
    public get ast() {
294
        return this.parser.ast;
696✔
295
    }
296

297
    /**
298
     * The full file contents
299
     */
300
    public fileContents: string;
301

302
    /**
303
     * Calculate the AST for this file
304
     * @param fileContents the xml source code to parse
305
     */
306
    public parse(fileContents: string) {
307
        this.fileContents = fileContents;
473✔
308

309
        this.parser.parse(fileContents, {
473✔
310
            srcPath: this.srcPath,
311
            destPath: this.destPath
312
        });
313

314
        this.program?.diagnostics.register(this.parser.diagnostics);
473!
315
        this.getCommentFlags(this.parser.tokens as any[]);
473✔
316
    }
317

318
    /**
319
     * Generate the code, map, and typedef for this file
320
     */
321
    public serialize(): SerializedCodeFile {
322
        const result = this.transpile();
27✔
323
        return {
27✔
324
            code: result?.code,
81!
325
            map: result?.map?.toString()
162!
326
        };
327
    }
328

329
    /**
330
     * Collect all bs: comment flags
331
     */
332
    public getCommentFlags(tokens: Array<IToken & { tokenType: TokenType }>) {
333
        const processor = new CommentFlagProcessor(this, ['<!--'], diagnosticCodes, [DiagnosticCodeMap.unknownDiagnosticCode, DiagnosticLegacyCodeMap.unknownDiagnosticCode]);
473✔
334

335
        this.commentFlags = [];
473✔
336
        for (let token of tokens) {
473✔
337
            if (token.tokenType.name === 'Comment') {
13,079✔
338
                processor.tryAdd(
7✔
339
                    //remove the close comment symbol
340
                    token.image.replace(/\-\-\>$/, ''),
341
                    //technically this range is 3 characters longer due to the removed `-->`, but that probably doesn't matter
342
                    this.parser.rangeFromToken(token)
343
                );
344
            }
345
        }
346
        this.commentFlags.push(...processor.commentFlags);
473✔
347
        this.program?.diagnostics.register(processor.diagnostics);
473!
348
    }
349

350
    private dependencyGraph: DependencyGraph;
351

352
    public onDependenciesChanged(event: DependencyChangedEvent) {
353
        this.logDebug('clear cache because dependency graph changed', event?.sourceKey);
926!
354
        this.cache.clear();
926✔
355
    }
356

357
    /**
358
     * Attach the file to the dependency graph so it can monitor changes.
359
     * Also notify the dependency graph of our current dependencies so other dependents can be notified.
360
     * @deprecated this does nothing. This functionality is now handled by the file api and will be deleted in v1
361
     */
362
    public attachDependencyGraph(dependencyGraph: DependencyGraph) {
363
        this.dependencyGraph = dependencyGraph;
467✔
364
    }
365

366
    /**
367
     * The list of files that this file depends on
368
     */
369
    public get dependencies() {
370
        const dependencies = [
932✔
371
            ...this.scriptTagImports.map(x => x.destPath.toLowerCase())
550✔
372
        ];
373
        //if autoImportComponentScript is enabled, add the .bs and .brs files with the same name
374
        if (this.program?.options?.autoImportComponentScript) {
932!
375
            dependencies.push(
278✔
376
                //add the codebehind file dependencies.
377
                //These are kind of optional, so it doesn't hurt to just add both extension versions
378
                this.destPath.replace(/\.xml$/i, '.bs').toLowerCase(),
379
                this.destPath.replace(/\.xml$/i, '.brs').toLowerCase()
380
            );
381
        }
382
        const len = dependencies.length;
932✔
383
        for (let i = 0; i < len; i++) {
932✔
384
            const dep = dependencies[i];
1,106✔
385

386
            //add a dependency on `d.bs` file for every `.brs` file
387
            if (dep.slice(-4).toLowerCase() === '.brs') {
1,106✔
388
                dependencies.push(util.getTypedefPath(dep));
524✔
389
            }
390
        }
391

392
        if (this.parentComponentName) {
932✔
393
            dependencies.push(this.parentComponentDependencyGraphKey);
840✔
394
        }
395
        return dependencies;
932✔
396
    }
397

398
    /**
399
     * A slight hack. Gives the Program a way to support multiple components with the same name
400
     * without causing major issues. A value of 0 will be ignored as part of the dependency graph key.
401
     * Howver, a nonzero value will be used as part of the dependency graph key so this component doesn't
402
     * collide with the primary component. For example, if there are three components with the same name, you will
403
     * have the following dependency graph keys: ["component:CustomGrid", "component:CustomGrid[1]", "component:CustomGrid[2]"]
404
     */
405
    public dependencyGraphIndex = -1;
535✔
406

407
    /**
408
     * The key used in the dependency graph for this file.
409
     * If we have a component name, we will use that so we can be discoverable by child components.
410
     * If we don't have a component name, use the destPath so at least we can self-validate
411
     */
412
    public get dependencyGraphKey() {
413
        let key: string;
414
        if (this.componentName) {
3,455✔
415
            key = `component:${this.componentName.text}`.toLowerCase();
3,383✔
416
        } else {
417
            key = this.destPath.toLowerCase();
72✔
418
        }
419
        //if our index is not zero, then we are not the primary component with that name, and need to
420
        //append our index to the dependency graph key as to prevent collisions in the program.
421
        if (this.dependencyGraphIndex !== 0) {
3,455✔
422
            key += '[' + this.dependencyGraphIndex + ']';
948✔
423
        }
424
        return key;
3,455✔
425
    }
426

427
    public set dependencyGraphKey(value) {
428
        //do nothing, we override this value in the getter
429
    }
430

431
    /**
432
     * The key used in the dependency graph for this component's parent.
433
     * If we have aparent, we will use that. If we don't, this will return undefined
434
     */
435
    public get parentComponentDependencyGraphKey() {
436
        if (this.parentComponentName) {
1,414✔
437
            return `component:${this.parentComponentName.text}`.toLowerCase();
1,380✔
438
        } else {
439
            return undefined;
34✔
440
        }
441
    }
442

443
    /**
444
     * Determines if this xml file has a reference to the specified file (or if it's itself)
445
     */
446
    public doesReferenceFile(file: BscFile) {
447
        return this.cache.getOrAdd(`doesReferenceFile: ${file.destPath}`, () => {
1✔
448
            if (file === this) {
1!
UNCOV
449
                return true;
×
450
            }
451
            let allDependencies = this.getOwnDependencies();
1✔
452
            for (let destPath of allDependencies) {
1✔
453
                if (destPath.toLowerCase() === file.destPath.toLowerCase()) {
2✔
454
                    return true;
1✔
455
                }
456
            }
457

458
            //if this is an xml file...do we extend the component it defines?
UNCOV
459
            if (path.extname(file.destPath).toLowerCase() === '.xml') {
×
460

461
                //didn't find any script imports for this file
UNCOV
462
                return false;
×
463
            }
UNCOV
464
            return false;
×
465
        });
466
    }
467

468
    /**
469
     * Get the parent component (the component this component extends)
470
     */
471
    public get parentComponent() {
472
        const result = this.cache.getOrAdd('parent', () => {
639✔
473
            return this.program.getComponent(this.parentComponentName?.text)?.file;
546✔
474
        });
475
        return result;
639✔
476
    }
477

478
    public getReferences(position: Position): Promise<Location[]> { //eslint-disable-line
479
        //TODO implement
UNCOV
480
        return null;
×
481
    }
482

483
    public getFunctionScopeAtPosition(position: Position, functionScopes?: FunctionScope[]): FunctionScope { //eslint-disable-line
484
        //TODO implement
UNCOV
485
        return null;
×
486
    }
487

488
    /**
489
     * Walk up the ancestor chain and aggregate all of the script tag imports
490
     */
491
    public getAncestorScriptTagImports(): FileReference[] {
492
        let result = [] as FileReference[];
34✔
493
        let parent = this.parentComponent;
34✔
494
        while (parent) {
34✔
495
            result.push(...parent.scriptTagImports);
34✔
496
            parent = parent.parentComponent;
34✔
497
        }
498
        return result;
34✔
499
    }
500

501
    /**
502
     * Remove this file from the dependency graph as a node
503
     */
504
    public detachDependencyGraph(dependencyGraph: DependencyGraph) {
UNCOV
505
        dependencyGraph.remove(this.dependencyGraphKey);
×
506

507
    }
508

509
    /**
510
     * Get the list of script imports that this file needs to include.
511
     * It compares the list of imports on this file to those of its parent,
512
     * and only includes the ones that are not found on the parent.
513
     * If no parent is found, all imports are returned
514
     */
515
    public getMissingImportsForTranspile() {
516
        let ownImports = this.getAvailableScriptImports();
27✔
517
        //add the bslib path to ownImports, it'll get filtered down below
518
        ownImports.push(this.program.bslibPkgPath);
27✔
519

520
        let parentImports = this.parentComponent?.getAvailableScriptImports() ?? [];
27!
521

522
        let parentMap = parentImports.reduce((map, destPath) => {
27✔
UNCOV
523
            map[destPath.toLowerCase()] = true;
×
UNCOV
524
            return map;
×
525
        }, {});
526

527
        //if the XML already has this import, skip this one
528
        let alreadyThereScriptImportMap = this.scriptTagImports.reduce((map, fileReference) => {
27✔
529
            map[fileReference.destPath.toLowerCase()] = true;
19✔
530
            return map;
19✔
531
        }, {});
532

533
        let resultMap = {};
27✔
534
        let result = [] as string[];
27✔
535
        for (let ownImport of ownImports) {
27✔
536
            const ownImportLower = ownImport.toLowerCase();
56✔
537
            if (
56✔
538
                //if the parent doesn't have this import
539
                !parentMap[ownImportLower] &&
148✔
540
                //the XML doesn't already have a script reference for this
541
                !alreadyThereScriptImportMap[ownImportLower] &&
542
                //the result doesn't already have this reference
543
                !resultMap[ownImportLower]
544
            ) {
545
                result.push(ownImport);
35✔
546
                resultMap[ownImportLower] = true;
35✔
547
            }
548
        }
549
        return result;
27✔
550
    }
551

552
    private logDebug(...args) {
553
        this.program?.logger?.debug('XmlFile', chalk.green(this.destPath), ...args);
954!
554
    }
555

556
    /**
557
     * Convert the brightscript/brighterscript source code into valid brightscript
558
     */
559
    public transpile(): CodeWithSourceMap {
560
        const state = new TranspileState(this.srcPath, this.program.options);
30✔
561

562
        let transpileResult: SourceNode | undefined;
563

564
        if (this.needsTranspiled) {
30✔
565
            transpileResult = util.sourceNodeFromTranspileResult(null, null, state.srcPath, this.parser.ast.transpile(state));
27✔
566
        } else if (this.program.options.sourceMap) {
3✔
567
            //emit code as-is with a simple map to the original file location
568
            transpileResult = util.simpleMap(state.srcPath, this.fileContents);
1✔
569
        } else {
570
            //simple SourceNode wrapping the entire file to simplify the logic below
571
            transpileResult = new SourceNode(null, null, state.srcPath, this.fileContents);
2✔
572
        }
573

574
        //add the source map comment if configured to emit sourcemaps
575
        if (this.program.options.sourceMap) {
30✔
576
            return new SourceNode(null, null, state.srcPath, [
4✔
577
                transpileResult,
578
                //add the sourcemap reference comment
579
                `<!--//# sourceMappingURL=./${path.basename(state.srcPath)}.map -->`
580
            ]).toStringWithSourceMap();
581
        } else {
582
            return {
26✔
583
                code: transpileResult.toString(),
584
                map: undefined
585
            };
586
        }
587
    }
588

589
    public dispose() {
590
        //unsubscribe from any DependencyGraph subscriptions
591
        this.unsubscribeFromDependencyGraph?.();
463!
592
    }
593
}
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