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

rokucommunity / brighterscript / #12716

14 Jun 2024 08:20PM UTC coverage: 85.629% (-2.3%) from 87.936%
#12716

push

web-flow
Merge 94311dc0a into 42db50190

10808 of 13500 branches covered (80.06%)

Branch coverage included in aggregate %.

6557 of 7163 new or added lines in 96 files covered. (91.54%)

83 existing lines in 17 files now uncovered.

12270 of 13451 relevant lines covered (91.22%)

26529.43 hits per line

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

85.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, 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) {
446!
39
            this.srcPath = s`${options.srcPath}`;
446✔
40
            this.destPath = s`${options.destPath}`;
446✔
41
            this.pkgPath = s`${options.pkgPath ?? options.destPath}`;
446!
42
            this.program = options.program;
446✔
43

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

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

53
    public type = 'XmlFile';
446✔
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();
446✔
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;
446✔
111

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

117
    public commentFlags = [] as CommentFlag[];
446✔
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;
446✔
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,289✔
130
            .map(tag => ({
726✔
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`, () => {
496✔
161
            const value = this.dependencyGraph.getAllDependencies(this.dependencyGraphKey, [this.parentComponentDependencyGraphKey]);
469✔
162
            return value;
469✔
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', () => {
28✔
176

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

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

191

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

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

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

202
    public functionScopes = [] as FunctionScope[];
446✔
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,953!
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,065!
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) {
33✔
226
            return this._needsTranspiled;
2✔
227
        }
228
        return !!(
31✔
229
            this.editor?.hasChanges || this.ast.componentElement?.scriptElements?.some(
166!
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;
576✔
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;
383✔
257

258
        this.parser.parse(fileContents, {
383✔
259
            srcPath: this.srcPath,
260
            destPath: this.destPath
261
        });
262
        const diagnostics = this.parser.diagnostics.map(diagnostic => ({
383✔
263
            ...diagnostic,
264
            file: this
265
        }));
266
        this.program?.diagnostics.register(diagnostics);
383!
267
        this.getCommentFlags(this.parser.tokens as any[]);
383✔
268
    }
269

270
    /**
271
     * Generate the code, map, and typedef for this file
272
     */
273
    public serialize(): SerializedCodeFile {
274
        const result = this.transpile();
26✔
275
        return {
26✔
276
            code: result?.code,
78!
277
            map: result?.map?.toString()
156!
278
        };
279
    }
280

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

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

302
    private dependencyGraph: DependencyGraph;
303

304
    public onDependenciesChanged(event: DependencyChangedEvent) {
305
        this.logDebug('clear cache because dependency graph changed');
764✔
306
        this.cache.clear();
764✔
307
    }
308

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

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

338
            //add a dependency on `d.bs` file for every `.brs` file
339
            if (dep.slice(-4).toLowerCase() === '.brs') {
990✔
340
                dependencies.push(util.getTypedefPath(dep));
516✔
341
            }
342
        }
343

344
        if (this.parentComponentName) {
752✔
345
            dependencies.push(this.parentComponentDependencyGraphKey);
700✔
346
        }
347
        return dependencies;
752✔
348
    }
349

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

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

379
    public set dependencyGraphKey(value) {
380
        //do nothing, we override this value in the getter
381
    }
382

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

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

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

413
                //didn't find any script imports for this file
414
                return false;
×
415
            }
416
            return false;
×
417
        });
418
    }
419

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

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

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

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

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

459
    }
460

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

472
        let parentImports = this.parentComponent?.getAvailableScriptImports() ?? [];
26!
473

474
        let parentMap = parentImports.reduce((map, destPath) => {
26✔
NEW
475
            map[destPath.toLowerCase()] = true;
×
UNCOV
476
            return map;
×
477
        }, {});
478

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

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

504
    private logDebug(...args) {
505
        this.program?.logger?.debug('XmlFile', chalk.green(this.destPath), ...args);
791!
506
    }
507

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

514
        let transpileResult: SourceNode | undefined;
515

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

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

541
    public dispose() {
542
        //unsubscribe from any DependencyGraph subscriptions
543
        this.unsubscribeFromDependencyGraph?.();
354!
544
    }
545
}
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