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

rokucommunity / brighterscript / #15046

03 Oct 2022 01:55PM UTC coverage: 87.532% (-0.3%) from 87.808%
#15046

push

TwitchBronBron
0.59.0

5452 of 6706 branches covered (81.3%)

Branch coverage included in aggregate %.

8259 of 8958 relevant lines covered (92.2%)

1521.92 hits per line

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

90.16
/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 { CompletionItem, Location, Position, Range } from 'vscode-languageserver';
5
import { DiagnosticCodeMap, diagnosticCodes } from '../DiagnosticMessages';
1✔
6
import type { FunctionScope } from '../FunctionScope';
7
import type { Callable, BsDiagnostic, File, FileReference, FunctionCall, CommentFlag } from '../interfaces';
8
import type { Program } from '../Program';
9
import util from '../util';
1✔
10
import SGParser, { rangeFromTokenValue } from '../parser/SGParser';
1✔
11
import chalk from 'chalk';
1✔
12
import { Cache } from '../Cache';
1✔
13
import type { DependencyGraph } from '../DependencyGraph';
14
import type { SGToken } from '../parser/SGTypes';
15
import { SGScript } from '../parser/SGTypes';
1✔
16
import { CommentFlagProcessor } from '../CommentFlagProcessor';
1✔
17
import type { IToken, TokenType } from 'chevrotain';
18
import { TranspileState } from '../parser/TranspileState';
1✔
19

20
export class XmlFile {
1✔
21
    constructor(
22
        public srcPath: string,
262✔
23
        /**
24
         * The absolute path to the file, relative to the pkg
25
         */
26
        public pkgPath: string,
262✔
27
        public program: Program
262✔
28
    ) {
29
        this.extension = path.extname(this.srcPath).toLowerCase();
262✔
30

31
        this.possibleCodebehindPkgPaths = [
262✔
32
            this.pkgPath.replace('.xml', '.bs'),
33
            this.pkgPath.replace('.xml', '.brs')
34
        ];
35
    }
36

37
    /**
38
     * The absolute path to the source location for this file
39
     * @deprecated use `srcPath` instead
40
     */
41
    public get pathAbsolute() {
42
        return this.srcPath;
×
43
    }
44
    public set pathAbsolute(value) {
45
        this.srcPath = value;
×
46
    }
47

48
    private cache = new Cache();
262✔
49

50
    /**
51
     * The list of possible autoImport codebehind pkg paths.
52
     */
53
    public possibleCodebehindPkgPaths: string[];
54

55
    /**
56
     * An unsubscribe function for the dependencyGraph subscription
57
     */
58
    private unsubscribeFromDependencyGraph: () => void;
59

60
    /**
61
     * Indicates whether this file needs to be validated.
62
     * Files are only ever validated a single time
63
     */
64
    public isValidated = false;
262✔
65

66
    /**
67
     * The extension for this file
68
     */
69
    public extension: string;
70

71
    public commentFlags = [] as CommentFlag[];
262✔
72

73
    /**
74
     * The list of script imports delcared in the XML of this file.
75
     * This excludes parent imports and auto codebehind imports
76
     */
77
    public get scriptTagImports(): FileReference[] {
78
        return this.parser.references.scriptTagImports
433✔
79
            .map(tag => ({
346✔
80
                ...tag,
81
                sourceFile: this
82
            }));
83
    }
84

85
    /**
86
     * List of all pkgPaths to scripts that this XmlFile depends, regardless of whether they are loaded in the program or not.
87
     * This includes own dependencies and all parent compoent dependencies
88
     * coming from:
89
     *  - script tags
90
     *  - implied codebehind file
91
     *  - import statements from imported scripts or their descendents
92
     */
93
    public getAllDependencies() {
94
        return this.cache.getOrAdd(`allScriptImports`, () => {
×
95
            const value = this.dependencyGraph.getAllDependencies(this.dependencyGraphKey);
×
96
            return value;
×
97
        });
98
    }
99

100
    /**
101
     * List of all pkgPaths to scripts that this XmlFile depends on directly, regardless of whether they are loaded in the program or not.
102
     * This does not account for parent component scripts
103
     * coming from:
104
     *  - script tags
105
     *  - implied codebehind file
106
     *  - import statements from imported scripts or their descendents
107
     */
108
    public getOwnDependencies() {
109
        return this.cache.getOrAdd(`ownScriptImports`, () => {
189✔
110
            const value = this.dependencyGraph.getAllDependencies(this.dependencyGraphKey, [this.parentComponentDependencyGraphKey]);
173✔
111
            return value;
173✔
112
        });
113
    }
114

115
    /**
116
     * List of all pkgPaths to scripts that this XmlFile depends on that are actually loaded into the program.
117
     * This does not account for parent component scripts.
118
     * coming from:
119
     *  - script tags
120
     *  - inferred codebehind file
121
     *  - import statements from imported scripts or their descendants
122
     */
123
    public getAvailableScriptImports() {
124
        return this.cache.getOrAdd('allAvailableScriptImports', () => {
20✔
125

126
            let allDependencies = this.getOwnDependencies()
19✔
127
                //skip typedef files
128
                .filter(x => util.getExtension(x) !== '.d.bs');
30✔
129

130
            let result = [] as string[];
19✔
131
            let filesInProgram = this.program.getFiles(allDependencies);
19✔
132
            for (let file of filesInProgram) {
19✔
133
                result.push(file.pkgPath);
21✔
134
            }
135
            this.logDebug('computed allAvailableScriptImports', () => result);
19✔
136
            return result;
19✔
137
        });
138
    }
139

140
    public getDiagnostics() {
141
        return [...this.diagnostics];
145✔
142
    }
143

144
    public addDiagnostics(diagnostics: BsDiagnostic[]) {
145
        this.diagnostics.push(...diagnostics);
3✔
146
    }
147

148
    /**
149
     * The range of the entire file
150
     */
151
    public fileRange: Range;
152

153
    /**
154
     * A collection of diagnostics related to this file
155
     */
156
    public diagnostics = [] as BsDiagnostic[];
262✔
157

158
    public parser = new SGParser();
262✔
159

160
    //TODO implement the xml CDATA parsing, which would populate this list
161
    public callables = [] as Callable[];
262✔
162

163
    //TODO implement the xml CDATA parsing, which would populate this list
164
    public functionCalls = [] as FunctionCall[];
262✔
165

166
    public functionScopes = [] as FunctionScope[];
262✔
167

168
    /**
169
     * The name of the component that this component extends.
170
     * Available after `parse()`
171
     */
172
    public get parentComponentName(): SGToken {
173
        return this.parser?.references.extends;
1,142!
174
    }
175

176
    /**
177
     * The name of the component declared in this xml file
178
     * Available after `parse()`
179
     */
180
    public get componentName(): SGToken {
181
        return this.parser?.references.name;
2,316!
182
    }
183

184
    /**
185
     * Does this file need to be transpiled?
186
     */
187
    public needsTranspiled = false;
262✔
188

189
    /**
190
     * The AST for this file
191
     */
192
    public get ast() {
193
        return this.parser.ast;
288✔
194
    }
195

196
    /**
197
     * The full file contents
198
     */
199
    public fileContents: string;
200

201
    /**
202
     * Calculate the AST for this file
203
     * @param fileContents
204
     */
205
    public parse(fileContents: string) {
206
        this.fileContents = fileContents;
201✔
207

208
        this.parser.parse(this.pkgPath, fileContents);
201✔
209
        this.diagnostics = this.parser.diagnostics.map(diagnostic => ({
201✔
210
            ...diagnostic,
211
            file: this
212
        }));
213

214
        this.getCommentFlags(this.parser.tokens as any[]);
201✔
215

216
        //needsTranspiled should be true if an import is brighterscript
217
        this.needsTranspiled = this.needsTranspiled || this.ast.component?.scripts?.some(
201✔
218
            script => script.type?.indexOf('brighterscript') > 0 || script.uri?.endsWith('.bs')
153✔
219
        );
220
    }
221

222
    /**
223
     * @deprecated logic has moved into XmlFileValidator, this is now an empty function
224
     */
225
    public validate() {
226

227
    }
228

229
    /**
230
     * Collect all bs: comment flags
231
     */
232
    public getCommentFlags(tokens: Array<IToken & { tokenType: TokenType }>) {
233
        const processor = new CommentFlagProcessor(this, ['<!--'], diagnosticCodes, [DiagnosticCodeMap.unknownDiagnosticCode]);
201✔
234

235
        this.commentFlags = [];
201✔
236
        for (let token of tokens) {
201✔
237
            if (token.tokenType.name === 'Comment') {
6,042✔
238
                processor.tryAdd(
7✔
239
                    //remove the close comment symbol
240
                    token.image.replace(/\-\-\>$/, ''),
241
                    rangeFromTokenValue(token)
242
                );
243
            }
244
        }
245
        this.commentFlags.push(...processor.commentFlags);
201✔
246
        this.diagnostics.push(...processor.diagnostics);
201✔
247
    }
248

249
    private dependencyGraph: DependencyGraph;
250

251
    /**
252
     * Attach the file to the dependency graph so it can monitor changes.
253
     * Also notify the dependency graph of our current dependencies so other dependents can be notified.
254
     */
255
    public attachDependencyGraph(dependencyGraph: DependencyGraph) {
256
        this.dependencyGraph = dependencyGraph;
195✔
257
        if (this.unsubscribeFromDependencyGraph) {
195✔
258
            this.unsubscribeFromDependencyGraph();
2✔
259
        }
260

261
        //anytime a dependency changes, clean up some cached values
262
        this.unsubscribeFromDependencyGraph = dependencyGraph.onchange(this.dependencyGraphKey, () => {
195✔
263
            this.logDebug('clear cache because dependency graph changed');
299✔
264
            this.cache.clear();
299✔
265
        });
266

267
        let dependencies = [
195✔
268
            ...this.scriptTagImports.map(x => x.pkgPath.toLowerCase())
149✔
269
        ];
270
        //if autoImportComponentScript is enabled, add the .bs and .brs files with the same name
271
        if (this.program.options.autoImportComponentScript) {
195✔
272
            dependencies.push(
15✔
273
                //add the codebehind file dependencies.
274
                //These are kind of optional, so it doesn't hurt to just add both extension versions
275
                this.pkgPath.replace(/\.xml$/i, '.bs').toLowerCase(),
276
                this.pkgPath.replace(/\.xml$/i, '.brs').toLowerCase()
277
            );
278
        }
279
        const len = dependencies.length;
195✔
280
        for (let i = 0; i < len; i++) {
195✔
281
            const dep = dependencies[i];
179✔
282

283
            //add a dependency on `d.bs` file for every `.brs` file
284
            if (dep.slice(-4).toLowerCase() === '.brs') {
179✔
285
                dependencies.push(util.getTypedefPath(dep));
120✔
286
            }
287
        }
288

289
        if (this.parentComponentName) {
195✔
290
            dependencies.push(this.parentComponentDependencyGraphKey);
164✔
291
        }
292
        this.dependencyGraph.addOrReplace(this.dependencyGraphKey, dependencies);
195✔
293
    }
294

295
    /**
296
     * A slight hack. Gives the Program a way to support multiple components with the same name
297
     * without causing major issues. A value of 0 will be ignored as part of the dependency graph key.
298
     * Howver, a nonzero value will be used as part of the dependency graph key so this component doesn't
299
     * collide with the primary component. For example, if there are three components with the same name, you will
300
     * have the following dependency graph keys: ["component:CustomGrid", "component:CustomGrid[1]", "component:CustomGrid[2]"]
301
     */
302
    public dependencyGraphIndex = -1;
262✔
303

304
    /**
305
     * The key used in the dependency graph for this file.
306
     * If we have a component name, we will use that so we can be discoverable by child components.
307
     * If we don't have a component name, use the pkgPath so at least we can self-validate
308
     */
309
    public get dependencyGraphKey() {
310
        let key: string;
311
        if (this.componentName) {
935✔
312
            key = `component:${this.componentName.text}`.toLowerCase();
872✔
313
        } else {
314
            key = this.pkgPath.toLowerCase();
63✔
315
        }
316
        //if our index is not zero, then we are not the primary component with that name, and need to
317
        //append our index to the dependency graph key as to prevent collisions in the program.
318
        if (this.dependencyGraphIndex !== 0) {
935✔
319
            key += '[' + this.dependencyGraphIndex + ']';
21✔
320
        }
321
        return key;
935✔
322
    }
323

324
    /**
325
     * The key used in the dependency graph for this component's parent.
326
     * If we have aparent, we will use that. If we don't, this will return undefined
327
     */
328
    public get parentComponentDependencyGraphKey() {
329
        if (this.parentComponentName) {
337✔
330
            return `component:${this.parentComponentName.text}`.toLowerCase();
318✔
331
        } else {
332
            return undefined;
19✔
333
        }
334
    }
335

336
    /**
337
     * Determines if this xml file has a reference to the specified file (or if it's itself)
338
     * @param file
339
     */
340
    public doesReferenceFile(file: File) {
341
        return this.cache.getOrAdd(`doesReferenceFile: ${file.pkgPath}`, () => {
1✔
342
            if (file === this) {
1!
343
                return true;
×
344
            }
345
            let allDependencies = this.getOwnDependencies();
1✔
346
            for (let importPkgPath of allDependencies) {
1✔
347
                if (importPkgPath.toLowerCase() === file.pkgPath.toLowerCase()) {
2✔
348
                    return true;
1✔
349
                }
350
            }
351

352
            //if this is an xml file...do we extend the component it defines?
353
            if (path.extname(file.pkgPath).toLowerCase() === '.xml') {
×
354

355
                //didn't find any script imports for this file
356
                return false;
×
357
            }
358
            return false;
×
359
        });
360
    }
361

362
    /**
363
     * Get all available completions for the specified position
364
     * @param lineIndex
365
     * @param columnIndex
366
     */
367
    public getCompletions(position: Position): CompletionItem[] {
368
        let scriptImport = util.getScriptImportAtPosition(this.scriptTagImports, position);
5✔
369
        if (scriptImport) {
5✔
370
            return this.program.getScriptImportCompletions(this.pkgPath, scriptImport);
3✔
371
        } else {
372
            return [];
2✔
373
        }
374
    }
375

376
    /**
377
     * Get the parent component (the component this component extends)
378
     */
379
    public get parentComponent() {
380
        const result = this.cache.getOrAdd('parent', () => {
226✔
381
            return this.program.getComponent(this.parentComponentName?.text)?.file;
142✔
382
        });
383
        return result;
226✔
384
    }
385

386
    public getReferences(position: Position): Promise<Location[]> { //eslint-disable-line
387
        //TODO implement
388
        return null;
×
389
    }
390

391
    public getFunctionScopeAtPosition(position: Position, functionScopes?: FunctionScope[]): FunctionScope { //eslint-disable-line
392
        //TODO implement
393
        return null;
×
394
    }
395

396
    /**
397
     * Walk up the ancestor chain and aggregate all of the script tag imports
398
     */
399
    public getAncestorScriptTagImports() {
400
        let result = [];
34✔
401
        let parent = this.parentComponent;
34✔
402
        while (parent) {
34✔
403
            result.push(...parent.scriptTagImports);
34✔
404
            parent = parent.parentComponent;
34✔
405
        }
406
        return result;
34✔
407
    }
408

409
    /**
410
     * Remove this file from the dependency graph as a node
411
     */
412
    public detachDependencyGraph(dependencyGraph: DependencyGraph) {
413
        dependencyGraph.remove(this.dependencyGraphKey);
×
414

415
    }
416

417
    /**
418
     * Get the list of script imports that this file needs to include.
419
     * It compares the list of imports on this file to those of its parent,
420
     * and only includes the ones that are not found on the parent.
421
     * If no parent is found, all imports are returned
422
     */
423
    private getMissingImportsForTranspile() {
424
        let ownImports = this.getAvailableScriptImports();
18✔
425
        //add the bslib path to ownImports, it'll get filtered down below
426
        ownImports.push(this.program.bslibPkgPath);
18✔
427

428
        let parentImports = this.parentComponent?.getAvailableScriptImports() ?? [];
18!
429

430
        let parentMap = parentImports.reduce((map, pkgPath) => {
18✔
431
            map[pkgPath.toLowerCase()] = true;
×
432
            return map;
×
433
        }, {});
434

435
        //if the XML already has this import, skip this one
436
        let alreadyThereScriptImportMap = this.scriptTagImports.reduce((map, fileReference) => {
18✔
437
            map[fileReference.pkgPath.toLowerCase()] = true;
13✔
438
            return map;
13✔
439
        }, {});
440

441
        let resultMap = {};
18✔
442
        let result = [] as string[];
18✔
443
        for (let ownImport of ownImports) {
18✔
444
            const ownImportLower = ownImport.toLowerCase();
36✔
445
            if (
36✔
446
                //if the parent doesn't have this import
447
                !parentMap[ownImportLower] &&
95✔
448
                //the XML doesn't already have a script reference for this
449
                !alreadyThereScriptImportMap[ownImportLower] &&
450
                //the result doesn't already have this reference
451
                !resultMap[ownImportLower]
452
            ) {
453
                result.push(ownImport);
22✔
454
                resultMap[ownImportLower] = true;
22✔
455
            }
456
        }
457
        return result;
18✔
458
    }
459

460
    private logDebug(...args) {
461
        this.program.logger.debug('XmlFile', chalk.green(this.pkgPath), ...args);
318✔
462
    }
463

464
    /**
465
     * Convert the brightscript/brighterscript source code into valid brightscript
466
     */
467
    public transpile(): CodeWithSourceMap {
468
        const state = new TranspileState(this.srcPath, this.program.options);
20✔
469

470
        const extraImportScripts = this.getMissingImportsForTranspile().map(uri => {
20✔
471
            const script = new SGScript();
22✔
472
            script.uri = util.getRokuPkgPath(uri.replace(/\.bs$/, '.brs'));
22✔
473
            return script;
22✔
474
        });
475

476
        let transpileResult: SourceNode | undefined;
477

478
        if (this.needsTranspiled || extraImportScripts.length > 0) {
20✔
479
            //temporarily add the missing imports as script tags
480
            const originalScripts = this.ast.component?.scripts ?? [];
17!
481
            this.ast.component.scripts = [
17✔
482
                ...originalScripts,
483
                ...extraImportScripts
484
            ];
485

486
            transpileResult = new SourceNode(null, null, state.srcPath, this.parser.ast.transpile(state));
17✔
487

488
            //restore the original scripts array
489
            this.ast.component.scripts = originalScripts;
17✔
490

491
        } else if (this.program.options.sourceMap) {
3✔
492
            //emit code as-is with a simple map to the original file location
493
            transpileResult = util.simpleMap(state.srcPath, this.fileContents);
1✔
494
        } else {
495
            //simple SourceNode wrapping the entire file to simplify the logic below
496
            transpileResult = new SourceNode(null, null, state.srcPath, this.fileContents);
2✔
497
        }
498

499
        //add the source map comment if configured to emit sourcemaps
500
        if (this.program.options.sourceMap) {
20✔
501
            return new SourceNode(null, null, state.srcPath, [
3✔
502
                transpileResult,
503
                //add the sourcemap reference comment
504
                `<!--//# sourceMappingURL=./${path.basename(state.srcPath)}.map -->`
505
            ]).toStringWithSourceMap();
506
        } else {
507
            return {
17✔
508
                code: transpileResult.toString(),
509
                map: undefined
510
            };
511
        }
512
    }
513

514
    public dispose() {
515
        //unsubscribe from any DependencyGraph subscriptions
516
        this.unsubscribeFromDependencyGraph?.();
173!
517
    }
518
}
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