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

rokucommunity / brighterscript / #15503

28 Mar 2026 09:13PM UTC coverage: 88.376% (-0.7%) from 89.035%
#15503

push

web-flow
Merge e74d0b0da into f3673e7df

8226 of 9814 branches covered (83.82%)

Branch coverage included in aggregate %.

103 of 184 new or added lines in 5 files covered. (55.98%)

7 existing lines in 3 files now uncovered.

10408 of 11271 relevant lines covered (92.34%)

1967.5 hits per line

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

76.74
/src/parser/SGTypes.ts
1
import { SourceNode } from 'source-map';
1✔
2
import type { Range } from 'vscode-languageserver';
3
import { createSGAttribute } from '../astUtils/creators';
1✔
4
import { isSGChildren, isSGCustomization, isSGField, isSGFunction, isSGInterface, isSGScript } from '../astUtils/xml';
1✔
5
import type { FileReference, TranspileResult } from '../interfaces';
6
import util from '../util';
1✔
7
import type { TranspileState } from './TranspileState';
8

9
export interface SGToken {
10
    text: string;
11
    range?: Range;
12
}
13

14
export interface SGAttribute {
15
    key: SGToken;
16
    openQuote?: SGToken;
17
    value: SGToken;
18
    closeQuote?: SGToken;
19
    range?: Range;
20
}
21

22
export class SGTag {
1✔
23

24
    constructor(
25
        public tag: SGToken,
802✔
26
        public attributes: SGAttribute[] = [],
802✔
27
        public range?: Range
802✔
28
    ) { }
29

30
    get id() {
31
        return this.getAttributeValue('id');
9✔
32
    }
33
    set id(value: string) {
34
        this.setAttribute('id', value);
×
35
    }
36

37
    getAttribute(name: string): SGAttribute | undefined {
38
        return this.attributes.find(att => att.key.text.toLowerCase() === name);
2,363✔
39
    }
40

41
    getAttributeValue(name: string): string | undefined {
42
        return this.getAttribute(name.toLowerCase())?.value?.text;
873✔
43
    }
44

45
    setAttribute(name: string, value: string) {
46
        const attr = this.getAttribute(name);
71✔
47
        if (attr) {
71✔
48
            if (value) {
3!
49
                attr.value = { text: value };
3✔
50
                attr.range = undefined;
3✔
51
            } else {
52
                this.attributes.splice(this.attributes.indexOf(attr), 1);
×
53
            }
54
        } else if (value) {
68!
55
            this.attributes.push({
68✔
56
                key: { text: name },
57
                value: { text: value }
58
            });
59
        }
60
    }
61

62
    transpile(state: TranspileState): SourceNode {
63
        return new SourceNode(null, null, state.srcPath, [
90✔
64
            state.indentText,
65
            '<',
66
            state.transpileToken(this.tag),
67
            ...this.transpileAttributes(state, this.attributes),
68
            ...this.transpileBody(state)
69
        ]);
70
    }
71

72
    protected transpileBody(state: TranspileState): (string | SourceNode)[] {
73
        return [' />\n'];
59✔
74
    }
75

76
    protected transpileAttributes(state: TranspileState, attributes: SGAttribute[]): (string | SourceNode)[] {
77
        const result = [];
113✔
78
        for (const attr of attributes) {
113✔
79
            result.push(
217✔
80
                ' ',
81
                state.transpileToken(attr.key),
82
                '=',
83
                state.transpileToken(attr.openQuote ?? { text: '"' }),
651✔
84
                state.transpileToken(attr.value),
85
                state.transpileToken(attr.closeQuote ?? { text: '"' })
651✔
86
            );
87
        }
88
        return result;
113✔
89
    }
90
}
91

92
export class SGProlog extends SGTag {
1✔
93

94
    transpile(state: TranspileState) {
95
        return new SourceNode(null, null, state.srcPath, [
23✔
96
            '<?xml',
97
            ...this.transpileAttributes(state, this.attributes),
98
            ' ?>\n'
99
        ]);
100
    }
101
}
102

103
export class SGNode extends SGTag {
1✔
104

105
    constructor(
106
        tag: SGToken,
107
        attributes?: SGAttribute[],
108
        public children: SGNode[] = [],
17!
109
        range?: Range
110
    ) {
111
        super(tag, attributes, range);
17✔
112
    }
113

114
    protected transpileBody(state: TranspileState): (string | SourceNode)[] {
115
        if (this.children.length > 0) {
11✔
116
            const body: (string | SourceNode)[] = ['>\n'];
5✔
117
            state.blockDepth++;
5✔
118
            body.push(...this.children.map(node => node.transpile(state)));
5✔
119
            state.blockDepth--;
5✔
120
            body.push(state.indentText, '</', this.tag.text, '>\n');
5✔
121
            return body;
5✔
122
        } else {
123
            return super.transpileBody(state);
6✔
124
        }
125
    }
126
}
127

128
export class SGChildren extends SGNode {
1✔
129

130
    constructor(
131
        tag: SGToken = { text: 'children' },
×
132
        children: SGNode[] = [],
×
133
        range?: Range
134
    ) {
135
        super(tag, [], children, range);
7✔
136
    }
137
}
138

139
export class SGScript extends SGTag {
1✔
140

141
    constructor(
142
        tag: SGToken = { text: 'script' },
34✔
143
        attributes?: SGAttribute[],
144
        public cdata?: SGToken,
242✔
145
        range?: Range
146
    ) {
147
        super(tag, attributes, range);
242✔
148
        if (!attributes) {
242✔
149
            this.type = 'text/brightscript';
34✔
150
        }
151
    }
152

153
    get type() {
154
        return this.getAttributeValue('type');
215✔
155
    }
156
    set type(value: string) {
157
        this.setAttribute('type', value);
37✔
158
    }
159

160
    get uri() {
161
        return this.getAttributeValue('uri');
189✔
162
    }
163
    set uri(value: string) {
164
        this.setAttribute('uri', value);
34✔
165
    }
166

167
    /**
168
     * The raw text content of the CDATA block, with the `<![CDATA[` and `]]>` wrappers stripped.
169
     * Returns undefined if this script tag has no CDATA content.
170
     */
171
    get cdataText(): string | undefined {
172
        return this.cdata?.text
15!
173
            .replace(/^<!\[CDATA\[/, '')
174
            .replace(/\]\]>$/, '');
175
    }
176

177
    protected transpileBody(state: TranspileState): (string | SourceNode)[] {
178
        if (this.cdata) {
49!
UNCOV
179
            return [
×
180
                '>',
181
                state.transpileToken(this.cdata),
182
                '</',
183
                this.tag.text,
184
                '>\n'
185
            ];
186
        } else {
187
            return super.transpileBody(state);
49✔
188
        }
189
    }
190

191
    protected transpileAttributes(state: TranspileState, attributes: SGAttribute[]): (string | SourceNode)[] {
192
        const modifiedAttributes = [] as SGAttribute[];
49✔
193
        let foundType = false;
49✔
194
        const bsExtensionRegexp = /\.bs$/i;
49✔
195

196
        for (const attr of attributes) {
49✔
197
            const lowerKey = attr.key.text.toLowerCase();
97✔
198
            if (lowerKey === 'uri' && bsExtensionRegexp.exec(attr.value.text)) {
97✔
199
                modifiedAttributes.push(
11✔
200
                    util.cloneSGAttribute(attr, attr.value.text.replace(bsExtensionRegexp, '.brs'))
201
                );
202
            } else if (lowerKey === 'type') {
86✔
203
                foundType = true;
48✔
204
                if (attr.value.text.toLowerCase().endsWith('brighterscript')) {
48✔
205
                    modifiedAttributes.push(
4✔
206
                        util.cloneSGAttribute(attr, 'text/brightscript')
207
                    );
208
                } else {
209
                    modifiedAttributes.push(attr);
44✔
210
                }
211
            } else {
212
                modifiedAttributes.push(attr);
38✔
213
            }
214
        }
215
        if (!foundType) {
49✔
216
            modifiedAttributes.push(
1✔
217
                createSGAttribute('type', 'text/brightscript')
218
            );
219
        }
220
        return super.transpileAttributes(state, modifiedAttributes);
49✔
221
    }
222
}
223

224
export class SGField extends SGTag {
1✔
225

226
    constructor(
227
        tag: SGToken = { text: 'field' },
×
228
        attributes: SGAttribute[] = [],
×
229
        range?: Range
230
    ) {
231
        super(tag, attributes, range);
11✔
232
    }
233

234
    get type() {
235
        return this.getAttributeValue('type');
9✔
236
    }
237
    set type(value: string) {
238
        this.setAttribute('type', value);
×
239
    }
240

241
    get alias() {
242
        return this.getAttributeValue('alias');
3✔
243
    }
244
    set alias(value: string) {
245
        this.setAttribute('alias', value);
×
246
    }
247

248
    get value() {
249
        return this.getAttributeValue('value');
×
250
    }
251
    set value(value: string) {
252
        this.setAttribute('value', value);
×
253
    }
254

255
    get onChange() {
256
        return this.getAttributeValue('onChange');
9✔
257
    }
258
    set onChange(value: string) {
259
        this.setAttribute('onChange', value);
×
260
    }
261

262
    get alwaysNotify() {
263
        return this.getAttributeValue('alwaysNotify');
×
264
    }
265
    set alwaysNotify(value: string) {
266
        this.setAttribute('alwaysNotify', value);
×
267
    }
268
}
269

270
export const SGFieldTypes = [
1✔
271
    'integer', 'int', 'longinteger', 'float', 'string', 'str', 'boolean', 'bool',
272
    'vector2d', 'color', 'time', 'uri', 'node', 'floatarray', 'intarray', 'boolarray',
273
    'stringarray', 'vector2darray', 'colorarray', 'timearray', 'nodearray', 'assocarray',
274
    'array', 'roarray', 'rect2d', 'rect2darray'
275
];
276

277
export class SGFunction extends SGTag {
1✔
278

279
    constructor(
280
        tag: SGToken = { text: 'function' },
×
281
        attributes: SGAttribute[] = [],
×
282
        range?: Range
283
    ) {
284
        super(tag, attributes, range);
24✔
285
    }
286

287
    get name() {
288
        return this.getAttributeValue('name');
37✔
289
    }
290
    set name(value: string) {
291
        this.setAttribute('name', value);
×
292
    }
293
}
294

295
export class SGInterface extends SGTag {
1✔
296

297
    fields: SGField[] = [];
20✔
298
    functions: SGFunction[] = [];
20✔
299

300
    constructor(
301
        tag: SGToken = { text: 'interface' },
×
302
        content?: SGTag[],
303
        range?: Range
304
    ) {
305
        super(tag, [], range);
20✔
306
        if (content) {
20!
307
            for (const tag of content) {
20✔
308
                if (isSGField(tag)) {
35✔
309
                    this.fields.push(tag);
11✔
310
                } else if (isSGFunction(tag)) {
24!
311
                    this.functions.push(tag);
24✔
312
                }
313
            }
314
        }
315
    }
316

317
    getField(id: string) {
318
        return this.fields.find(field => field.id === id);
×
319
    }
320
    setField(id: string, type: string, onChange?: string, alwaysNotify?: boolean, alias?: string) {
321
        let field = this.getField(id);
×
322
        if (!field) {
×
323
            field = new SGField();
×
324
            field.id = id;
×
325
            this.fields.push(field);
×
326
        }
327
        field.type = type;
×
328
        field.onChange = onChange;
×
329
        if (alwaysNotify === undefined) {
×
330
            field.alwaysNotify = undefined;
×
331
        } else {
332
            field.alwaysNotify = alwaysNotify ? 'true' : 'false';
×
333
        }
334
        field.alias = alias;
×
335
    }
336

337
    getFunction(name: string) {
338
        return this.functions.find(field => field.name === name);
×
339
    }
340
    setFunction(name: string) {
341
        let func = this.getFunction(name);
×
342
        if (!func) {
×
343
            func = new SGFunction();
×
344
            func.name = name;
×
345
            this.functions.push(func);
×
346
        }
347
    }
348

349
    protected transpileBody(state: TranspileState): (string | SourceNode)[] {
350
        const body: (string | SourceNode)[] = ['>\n'];
2✔
351
        state.blockDepth++;
2✔
352
        if (this.fields.length > 0) {
2!
353
            body.push(...this.fields.map(node => node.transpile(state)));
2✔
354
        }
355
        if (this.functions.length > 0) {
2!
356
            body.push(...this.functions.map(node => node.transpile(state)));
2✔
357
        }
358
        state.blockDepth--;
2✔
359
        body.push(state.indentText, '</', this.tag.text, '>\n');
2✔
360
        return body;
2✔
361
    }
362
}
363

364
export class SGComponent extends SGTag {
1✔
365
    constructor(
366
        tag: SGToken = { text: 'component' },
×
367
        attributes?: SGAttribute[],
368
        content?: SGTag[],
369
        range?: Range
370
    ) {
371
        super(tag, attributes, range);
268✔
372
        if (content) {
268!
373
            for (const tag of content) {
268✔
374
                if (isSGInterface(tag)) {
236✔
375
                    this.api = tag;
20✔
376
                } else if (isSGScript(tag)) {
216✔
377
                    this.scripts.push(tag);
207✔
378
                } else if (isSGChildren(tag)) {
9✔
379
                    this.children = tag;
7✔
380
                } else if (isSGCustomization(tag)) {
2!
381
                    this.customizations.push(tag);
2✔
382
                }
383
            }
384
        }
385
    }
386

387
    public api: SGInterface;
388

389
    public scripts: SGScript[] = [];
268✔
390

391
    public children: SGChildren;
392

393
    public customizations: SGNode[] = [];
268✔
394

395
    get name() {
396
        return this.getAttributeValue('name');
201✔
397
    }
398
    set name(value: string) {
399
        this.setAttribute('name', value);
×
400
    }
401

402
    get extends() {
403
        return this.getAttributeValue('extends');
201✔
404
    }
405
    set extends(value: string) {
406
        this.setAttribute('extends', value);
×
407
    }
408

409
    protected transpileBody(state: TranspileState): (string | SourceNode)[] {
410
        const body: (string | SourceNode)[] = ['>\n'];
24✔
411
        state.blockDepth++;
24✔
412
        if (this.api) {
24✔
413
            body.push(this.api.transpile(state));
2✔
414
        }
415
        if (this.scripts.length > 0) {
24!
416
            body.push(...this.scripts.map(node => node.transpile(state)));
49✔
417
        }
418
        if (this.children) {
24✔
419
            body.push(this.children.transpile(state));
4✔
420
        }
421
        if (this.customizations.length > 0) {
24✔
422
            body.push(...this.customizations.map(node => node.transpile(state)));
2✔
423
        }
424
        state.blockDepth--;
24✔
425
        body.push(state.indentText, '</', this.tag.text, '>\n');
24✔
426
        return body;
24✔
427
    }
428
}
429

430
export interface SGReferences {
431
    name?: SGToken;
432
    extends?: SGToken;
433
    scriptTagImports: Pick<FileReference, 'pkgPath' | 'text' | 'filePathRange'>[];
434
}
435

436
export class SGAst {
1✔
437

438
    constructor(
439
        public prolog?: SGProlog,
638✔
440
        public root?: SGTag,
638✔
441
        public component?: SGComponent
638✔
442
    ) {
443
    }
444

445
    transpile(state: TranspileState) {
446
        const chunks = [] as TranspileResult;
24✔
447
        //write XML prolog
448
        if (this.prolog) {
24✔
449
            chunks.push(this.prolog.transpile(state));
23✔
450
        }
451
        if (this.component) {
24!
452
            //write content
453
            chunks.push(this.component.transpile(state));
24✔
454
        }
455
        return chunks;
24✔
456
    }
457
}
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