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

rokucommunity / brighterscript / #13117

01 Oct 2024 08:24AM UTC coverage: 86.842% (-1.4%) from 88.193%
#13117

push

web-flow
Merge abd960cd5 into 3a2dc7282

11537 of 14048 branches covered (82.13%)

Branch coverage included in aggregate %.

6991 of 7582 new or added lines in 100 files covered. (92.21%)

83 existing lines in 18 files now uncovered.

12692 of 13852 relevant lines covered (91.63%)

29478.96 hits per line

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

85.93
/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, 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 { 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

22
export class XmlFile implements BscFile {
1✔
23
    /**
24
     * Create a new instance of BrsFile
25
     */
26
    constructor(options: {
27
        /**
28
         * 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').
29
         */
30
        srcPath: string;
31
        /**
32
         * The absolute path to the file on-device (i.e. 'source/main.brs') without the leading `pkg:/`
33
         */
34
        destPath: string;
35
        pkgPath?: string;
36
        program: Program;
37
    }) {
38
        if (options) {
450!
39
            this.srcPath = s`${options.srcPath}`;
450✔
40
            this.destPath = s`${options.destPath}`;
450✔
41
            this.pkgPath = s`${options.pkgPath ?? options.destPath}`;
450!
42
            this.program = options.program;
450✔
43

44
            this.extension = path.extname(this.srcPath).toLowerCase();
450✔
45

46
            this.possibleCodebehindDestPaths = [
450✔
47
                this.pkgPath.replace(/\.xml$/, '.bs'),
48
                this.pkgPath.replace(/\.xml$/, '.brs')
49
            ];
50
        }
51
    }
52

53
    public type = 'XmlFile';
450✔
54

55
    /**
56
     * 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').
57
     */
58
    public srcPath: string;
59
    /**
60
     * The absolute path to the file on-device (i.e. 'source/main.brs') without the leading `pkg:/`
61
     */
62
    public destPath: string;
63
    public pkgPath: string;
64

65
    public program: Program;
66

67
    /**
68
     * An editor assigned during the build flow that manages edits that will be undone once the build process is complete.
69
     */
70
    public editor?: Editor;
71

72
    /**
73
     * The absolute path to the source location for this file
74
     * @deprecated use `srcPath` instead
75
     */
76
    public get pathAbsolute() {
77
        return this.srcPath;
×
78
    }
79
    public set pathAbsolute(value) {
80
        this.srcPath = value;
×
81
    }
82

83
    private cache = new Cache();
450✔
84

85
    /**
86
     * The list of possible autoImport codebehind pkg paths.
87
     * @deprecated use `possibleCodebehindDestPaths` instead.
88
     */
89
    public get possibleCodebehindPkgPaths() {
NEW
90
        return this.possibleCodebehindDestPaths;
×
91
    }
92
    public set possibleCodebehindPkgPaths(value) {
NEW
93
        this.possibleCodebehindDestPaths = value;
×
94
    }
95

96
    /**
97
     * The list of possible autoImport codebehind destPath values
98
     */
99
    public possibleCodebehindDestPaths: string[];
100

101
    /**
102
     * An unsubscribe function for the dependencyGraph subscription
103
     */
104
    private unsubscribeFromDependencyGraph: () => void;
105

106
    /**
107
     * Indicates whether this file needs to be validated.
108
     * Files are only ever validated a single time
109
     */
110
    public isValidated = false;
450✔
111

112
    /**
113
     * The extension for this file
114
     */
115
    public extension: string;
116

117
    public commentFlags = [] as CommentFlag[];
450✔
118

119
    /**
120
     * 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.
121
     */
122
    readonly canBePruned = false;
450✔
123

124
    /**
125
     * The list of script imports delcared in the XML of this file.
126
     * This excludes parent imports and auto codebehind imports
127
     */
128
    public get scriptTagImports(): FileReference[] {
129
        return this.parser.references.scriptTagImports
1,300✔
130
            .map(tag => ({
732✔
131
                ...tag,
132
                sourceFile: this
133
            }));
134
    }
135

136
    /**
137
     * List of all `destPath` values pointing to scripts that this XmlFile depends on, regardless of whether they are loaded in the program or not.
138
     * This includes own dependencies and all parent compoent dependencies
139
     * coming from:
140
     *  - script tags
141
     *  - implied codebehind file
142
     *  - import statements from imported scripts or their descendents
143
     */
144
    public getAllDependencies() {
145
        return this.cache.getOrAdd(`allScriptImports`, () => {
×
146
            const value = this.dependencyGraph.getAllDependencies(this.dependencyGraphKey);
×
147
            return value;
×
148
        });
149
    }
150

151
    /**
152
     * List of all destPaths to scripts that this XmlFile depends on directly, regardless of whether they are loaded in the program or not.
153
     * This does not account for parent component scripts
154
     * coming from:
155
     *  - script tags
156
     *  - implied codebehind file
157
     *  - import statements from imported scripts or their descendents
158
     */
159
    public getOwnDependencies() {
160
        return this.cache.getOrAdd(`ownScriptImports`, () => {
500✔
161
            const value = this.dependencyGraph.getAllDependencies(this.dependencyGraphKey, [this.parentComponentDependencyGraphKey]);
472✔
162
            return value;
472✔
163
        });
164
    }
165

166
    /**
167
     * List of all destPaths to scripts that this XmlFile depends on that are actually loaded into the program.
168
     * This does not account for parent component scripts.
169
     * coming from:
170
     *  - script tags
171
     *  - inferred codebehind file
172
     *  - import statements from imported scripts or their descendants
173
     */
174
    public getAvailableScriptImports() {
175
        return this.cache.getOrAdd('allAvailableScriptImports', () => {
29✔
176

177
            let allDependencies = this.getOwnDependencies()
28✔
178
                //skip typedef files
179
                .filter(x => util.getExtension(x) !== '.d.bs');
49✔
180

181
            let result = [] as string[];
28✔
182
            let filesInProgram = this.program.getFiles(allDependencies);
28✔
183
            for (let file of filesInProgram) {
28✔
184
                result.push(file.destPath);
32✔
185
            }
186
            this.logDebug('computed allAvailableScriptImports', () => result);
28✔
187
            return result;
28✔
188
        });
189
    }
190

191

192
    /**
193
     * The range of the entire file
194
     */
195
    public fileRange: Range;
196

197
    public parser = new SGParser();
450✔
198

199
    //TODO implement the xml CDATA parsing, which would populate this list
200
    public callables = [] as Callable[];
450✔
201

202
    public functionScopes = [] as FunctionScope[];
450✔
203

204
    /**
205
     * The name of the component that this component extends.
206
     * Available after `parse()`
207
     */
208
    public get parentComponentName(): SGToken {
209
        return this.parser?.references.extends;
3,988!
210
    }
211

212
    /**
213
     * The name of the component declared in this xml file
214
     * Available after `parse()`
215
     */
216
    public get componentName(): SGToken {
217
        return this.parser?.references.name;
7,135!
218
    }
219

220
    /**
221
     * Does this file need to be transpiled?
222
     * @deprecated use the `.editor` property to push changes to the file, which will force transpilation
223
     */
224
    public get needsTranspiled() {
225
        if (this._needsTranspiled !== undefined) {
34✔
226
            return this._needsTranspiled;
2✔
227
        }
228
        return !!(
32✔
229
            this.editor?.hasChanges || this.ast.componentElement?.scriptElements?.some(
170!
230
                script => script.type?.indexOf('brighterscript') > 0 || script.uri?.endsWith('.bs')
4!
231
            )
232
        );
233
    }
234
    public set needsTranspiled(value) {
235
        this._needsTranspiled = value;
2✔
236
    }
237
    public _needsTranspiled: boolean;
238

239
    /**
240
     * The AST for this file
241
     */
242
    public get ast() {
243
        return this.parser.ast;
582✔
244
    }
245

246
    /**
247
     * The full file contents
248
     */
249
    public fileContents: string;
250

251
    /**
252
     * Calculate the AST for this file
253
     * @param fileContents the xml source code to parse
254
     */
255
    public parse(fileContents: string) {
256
        this.fileContents = fileContents;
387✔
257

258
        this.parser.parse(fileContents, {
387✔
259
            srcPath: this.srcPath,
260
            destPath: this.destPath
261
        });
262

263
        this.program?.diagnostics.register(this.parser.diagnostics);
387!
264
        this.getCommentFlags(this.parser.tokens as any[]);
387✔
265
    }
266

267
    /**
268
     * Generate the code, map, and typedef for this file
269
     */
270
    public serialize(): SerializedCodeFile {
271
        const result = this.transpile();
27✔
272
        return {
27✔
273
            code: result?.code,
81!
274
            map: result?.map?.toString()
162!
275
        };
276
    }
277

278
    /**
279
     * Collect all bs: comment flags
280
     */
281
    public getCommentFlags(tokens: Array<IToken & { tokenType: TokenType }>) {
282
        const processor = new CommentFlagProcessor(this, ['<!--'], diagnosticCodes, [DiagnosticCodeMap.unknownDiagnosticCode]);
387✔
283

284
        this.commentFlags = [];
387✔
285
        for (let token of tokens) {
387✔
286
            if (token.tokenType.name === 'Comment') {
10,290✔
287
                processor.tryAdd(
7✔
288
                    //remove the close comment symbol
289
                    token.image.replace(/\-\-\>$/, ''),
290
                    //technically this range is 3 characters longer due to the removed `-->`, but that probably doesn't matter
291
                    this.parser.rangeFromToken(token)
292
                );
293
            }
294
        }
295
        this.commentFlags.push(...processor.commentFlags);
387✔
296
        this.program?.diagnostics.register(processor.diagnostics);
387!
297
    }
298

299
    private dependencyGraph: DependencyGraph;
300

301
    public onDependenciesChanged(event: DependencyChangedEvent) {
302
        this.logDebug('clear cache because dependency graph changed');
770✔
303
        this.cache.clear();
770✔
304
    }
305

306
    /**
307
     * Attach the file to the dependency graph so it can monitor changes.
308
     * Also notify the dependency graph of our current dependencies so other dependents can be notified.
309
     * @deprecated this does nothing. This functionality is now handled by the file api and will be deleted in v1
310
     */
311
    public attachDependencyGraph(dependencyGraph: DependencyGraph) {
312
        this.dependencyGraph = dependencyGraph;
381✔
313
    }
314

315
    /**
316
     * The list of files that this file depends on
317
     */
318
    public get dependencies() {
319
        const dependencies = [
760✔
320
            ...this.scriptTagImports.map(x => x.destPath.toLowerCase())
446✔
321
        ];
322
        //if autoImportComponentScript is enabled, add the .bs and .brs files with the same name
323
        if (this.program?.options?.autoImportComponentScript) {
760!
324
            dependencies.push(
278✔
325
                //add the codebehind file dependencies.
326
                //These are kind of optional, so it doesn't hurt to just add both extension versions
327
                this.destPath.replace(/\.xml$/i, '.bs').toLowerCase(),
328
                this.destPath.replace(/\.xml$/i, '.brs').toLowerCase()
329
            );
330
        }
331
        const len = dependencies.length;
760✔
332
        for (let i = 0; i < len; i++) {
760✔
333
            const dep = dependencies[i];
1,002✔
334

335
            //add a dependency on `d.bs` file for every `.brs` file
336
            if (dep.slice(-4).toLowerCase() === '.brs') {
1,002✔
337
                dependencies.push(util.getTypedefPath(dep));
520✔
338
            }
339
        }
340

341
        if (this.parentComponentName) {
760✔
342
            dependencies.push(this.parentComponentDependencyGraphKey);
708✔
343
        }
344
        return dependencies;
760✔
345
    }
346

347
    /**
348
     * A slight hack. Gives the Program a way to support multiple components with the same name
349
     * without causing major issues. A value of 0 will be ignored as part of the dependency graph key.
350
     * Howver, a nonzero value will be used as part of the dependency graph key so this component doesn't
351
     * collide with the primary component. For example, if there are three components with the same name, you will
352
     * have the following dependency graph keys: ["component:CustomGrid", "component:CustomGrid[1]", "component:CustomGrid[2]"]
353
     */
354
    public dependencyGraphIndex = -1;
450✔
355

356
    /**
357
     * The key used in the dependency graph for this file.
358
     * If we have a component name, we will use that so we can be discoverable by child components.
359
     * If we don't have a component name, use the destPath so at least we can self-validate
360
     */
361
    public get dependencyGraphKey() {
362
        let key: string;
363
        if (this.componentName) {
2,480✔
364
            key = `component:${this.componentName.text}`.toLowerCase();
2,408✔
365
        } else {
366
            key = this.destPath.toLowerCase();
72✔
367
        }
368
        //if our index is not zero, then we are not the primary component with that name, and need to
369
        //append our index to the dependency graph key as to prevent collisions in the program.
370
        if (this.dependencyGraphIndex !== 0) {
2,480✔
371
            key += '[' + this.dependencyGraphIndex + ']';
774✔
372
        }
373
        return key;
2,480✔
374
    }
375

376
    public set dependencyGraphKey(value) {
377
        //do nothing, we override this value in the getter
378
    }
379

380
    /**
381
     * The key used in the dependency graph for this component's parent.
382
     * If we have aparent, we will use that. If we don't, this will return undefined
383
     */
384
    public get parentComponentDependencyGraphKey() {
385
        if (this.parentComponentName) {
1,180✔
386
            return `component:${this.parentComponentName.text}`.toLowerCase();
1,166✔
387
        } else {
388
            return undefined;
14✔
389
        }
390
    }
391

392
    /**
393
     * Determines if this xml file has a reference to the specified file (or if it's itself)
394
     */
395
    public doesReferenceFile(file: BscFile) {
396
        return this.cache.getOrAdd(`doesReferenceFile: ${file.destPath}`, () => {
1✔
397
            if (file === this) {
1!
398
                return true;
×
399
            }
400
            let allDependencies = this.getOwnDependencies();
1✔
401
            for (let destPath of allDependencies) {
1✔
402
                if (destPath.toLowerCase() === file.destPath.toLowerCase()) {
2✔
403
                    return true;
1✔
404
                }
405
            }
406

407
            //if this is an xml file...do we extend the component it defines?
NEW
408
            if (path.extname(file.destPath).toLowerCase() === '.xml') {
×
409

410
                //didn't find any script imports for this file
411
                return false;
×
412
            }
413
            return false;
×
414
        });
415
    }
416

417
    /**
418
     * Get the parent component (the component this component extends)
419
     */
420
    public get parentComponent() {
421
        const result = this.cache.getOrAdd('parent', () => {
530✔
422
            return this.program.getComponent(this.parentComponentName?.text)?.file;
439✔
423
        });
424
        return result;
530✔
425
    }
426

427
    public getReferences(position: Position): Promise<Location[]> { //eslint-disable-line
428
        //TODO implement
429
        return null;
×
430
    }
431

432
    public getFunctionScopeAtPosition(position: Position, functionScopes?: FunctionScope[]): FunctionScope { //eslint-disable-line
433
        //TODO implement
434
        return null;
×
435
    }
436

437
    /**
438
     * Walk up the ancestor chain and aggregate all of the script tag imports
439
     */
440
    public getAncestorScriptTagImports(): FileReference[] {
441
        let result = [] as FileReference[];
34✔
442
        let parent = this.parentComponent;
34✔
443
        while (parent) {
34✔
444
            result.push(...parent.scriptTagImports);
34✔
445
            parent = parent.parentComponent;
34✔
446
        }
447
        return result;
34✔
448
    }
449

450
    /**
451
     * Remove this file from the dependency graph as a node
452
     */
453
    public detachDependencyGraph(dependencyGraph: DependencyGraph) {
454
        dependencyGraph.remove(this.dependencyGraphKey);
×
455

456
    }
457

458
    /**
459
     * Get the list of script imports that this file needs to include.
460
     * It compares the list of imports on this file to those of its parent,
461
     * and only includes the ones that are not found on the parent.
462
     * If no parent is found, all imports are returned
463
     */
464
    public getMissingImportsForTranspile() {
465
        let ownImports = this.getAvailableScriptImports();
27✔
466
        //add the bslib path to ownImports, it'll get filtered down below
467
        ownImports.push(this.program.bslibPkgPath);
27✔
468

469
        let parentImports = this.parentComponent?.getAvailableScriptImports() ?? [];
27!
470

471
        let parentMap = parentImports.reduce((map, destPath) => {
27✔
NEW
472
            map[destPath.toLowerCase()] = true;
×
UNCOV
473
            return map;
×
474
        }, {});
475

476
        //if the XML already has this import, skip this one
477
        let alreadyThereScriptImportMap = this.scriptTagImports.reduce((map, fileReference) => {
27✔
478
            map[fileReference.destPath.toLowerCase()] = true;
19✔
479
            return map;
19✔
480
        }, {});
481

482
        let resultMap = {};
27✔
483
        let result = [] as string[];
27✔
484
        for (let ownImport of ownImports) {
27✔
485
            const ownImportLower = ownImport.toLowerCase();
56✔
486
            if (
56✔
487
                //if the parent doesn't have this import
488
                !parentMap[ownImportLower] &&
148✔
489
                //the XML doesn't already have a script reference for this
490
                !alreadyThereScriptImportMap[ownImportLower] &&
491
                //the result doesn't already have this reference
492
                !resultMap[ownImportLower]
493
            ) {
494
                result.push(ownImport);
35✔
495
                resultMap[ownImportLower] = true;
35✔
496
            }
497
        }
498
        return result;
27✔
499
    }
500

501
    private logDebug(...args) {
502
        this.program?.logger?.debug('XmlFile', chalk.green(this.destPath), ...args);
798!
503
    }
504

505
    /**
506
     * Convert the brightscript/brighterscript source code into valid brightscript
507
     */
508
    public transpile(): CodeWithSourceMap {
509
        const state = new TranspileState(this.srcPath, this.program.options);
30✔
510

511
        let transpileResult: SourceNode | undefined;
512

513
        if (this.needsTranspiled) {
30✔
514
            transpileResult = util.sourceNodeFromTranspileResult(null, null, state.srcPath, this.parser.ast.transpile(state));
27✔
515
        } else if (this.program.options.sourceMap) {
3✔
516
            //emit code as-is with a simple map to the original file location
517
            transpileResult = util.simpleMap(state.srcPath, this.fileContents);
1✔
518
        } else {
519
            //simple SourceNode wrapping the entire file to simplify the logic below
520
            transpileResult = new SourceNode(null, null, state.srcPath, this.fileContents);
2✔
521
        }
522

523
        //add the source map comment if configured to emit sourcemaps
524
        if (this.program.options.sourceMap) {
30✔
525
            return new SourceNode(null, null, state.srcPath, [
4✔
526
                transpileResult,
527
                //add the sourcemap reference comment
528
                `<!--//# sourceMappingURL=./${path.basename(state.srcPath)}.map -->`
529
            ]).toStringWithSourceMap();
530
        } else {
531
            return {
26✔
532
                code: transpileResult.toString(),
533
                map: undefined
534
            };
535
        }
536
    }
537

538
    public dispose() {
539
        //unsubscribe from any DependencyGraph subscriptions
540
        this.unsubscribeFromDependencyGraph?.();
358!
541
    }
542
}
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