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

rokucommunity / brighterscript / #13234

28 Oct 2024 07:06PM UTC coverage: 89.066% (+2.2%) from 86.866%
#13234

push

web-flow
Merge 9bcb77aad into 9ec6f722c

7233 of 8558 branches covered (84.52%)

Branch coverage included in aggregate %.

34 of 34 new or added lines in 5 files covered. (100.0%)

543 existing lines in 53 files now uncovered.

9621 of 10365 relevant lines covered (92.82%)

1782.52 hits per line

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

95.39
/src/parser/SGParser.ts
1
import type { AttributeCstNode, ContentCstNode, DocumentCstNode, ElementCstNode } from '@xml-tools/parser';
2
import * as parser from '@xml-tools/parser';
1✔
3
import { DiagnosticMessages } from '../DiagnosticMessages';
1✔
4
import type { Diagnostic, Range } from 'vscode-languageserver';
5
import util from '../util';
1✔
6
import { SGAst, SGProlog, SGChildren, SGComponent, SGField, SGFunction, SGInterface, SGNode, SGScript } from './SGTypes';
1✔
7
import type { SGTag, SGToken, SGAttribute, SGReferences } from './SGTypes';
8
import { isSGComponent } from '../astUtils/xml';
1✔
9

10
export default class SGParser {
1✔
11

12
    /**
13
     * The AST of the XML document, not including the inline scripts
14
     */
15
    public ast: SGAst = new SGAst();
284✔
16

17
    public tokens: IToken[];
18

19
    /**
20
     * The list of diagnostics found during the parse process
21
     */
22
    public diagnostics = [] as Diagnostic[];
284✔
23

24
    private pkgPath: string;
25

26
    private _references: SGReferences;
27

28
    /**
29
     * These are initially extracted during parse-time, but will also be dynamically regenerated if need be.
30
     *
31
     * If a plugin modifies the AST, then the plugin should call SGAst#invalidateReferences() to force this object to refresh
32
     */
33
    get references(): SGReferences {
34
        if (this._references === undefined) {
4,423✔
35
            this.findReferences();
213✔
36
        }
37
        return this._references;
4,423✔
38
    }
39

40

41
    /**
42
     * Invalidates (clears) the references collection. This should be called anytime the AST has been manipulated.
43
     */
44
    invalidateReferences() {
UNCOV
45
        this._references = undefined;
×
46
    }
47

48
    /**
49
     * Walk the AST to extract references to useful bits of information
50
     */
51
    private findReferences() {
52
        this._references = emptySGReferences();
213✔
53

54
        const { component } = this.ast;
213✔
55
        if (!component) {
213✔
56
            return;
11✔
57
        }
58

59
        const nameAttr = component.getAttribute('name');
202✔
60
        if (nameAttr?.value) {
202✔
61
            this._references.name = nameAttr.value;
196✔
62
        }
63
        const extendsAttr = component.getAttribute('extends');
202✔
64
        if (extendsAttr?.value) {
202✔
65
            this._references.extends = extendsAttr.value;
182✔
66
        }
67

68
        for (const script of component.scripts) {
202✔
69
            const uriAttr = script.getAttribute('uri');
164✔
70
            if (uriAttr) {
164✔
71
                const uri = uriAttr.value.text;
163✔
72
                this._references.scriptTagImports.push({
163✔
73
                    filePathRange: uriAttr.value.range,
74
                    text: uri,
75
                    pkgPath: util.getPkgPathFromTarget(this.pkgPath, uri)
76
                });
77
            }
78
        }
79
    }
80

81
    public parse(pkgPath: string, fileContents: string) {
82
        this.pkgPath = pkgPath;
220✔
83
        this.diagnostics = [];
220✔
84

85
        const { cst, tokenVector, lexErrors, parseErrors } = parser.parse(fileContents);
220✔
86
        this.tokens = tokenVector;
220✔
87

88
        if (lexErrors.length) {
220✔
89
            for (const err of lexErrors) {
1✔
90
                this.diagnostics.push({
1✔
91
                    ...DiagnosticMessages.xmlGenericParseError(`Syntax error: ${err.message}`),
92
                    range: util.createRange(
93
                        err.line - 1,
94
                        err.column,
95
                        err.line - 1,
96
                        err.column + err.length
97
                    )
98
                });
99
            }
100
        }
101
        if (parseErrors.length) {
220✔
102
            const err = parseErrors[0];
15✔
103
            const token = err.token;
15✔
104
            this.diagnostics.push({
15✔
105
                ...DiagnosticMessages.xmlGenericParseError(`Syntax error: ${err.message}`),
106
                range: !isNaN(token.startLine) ? rangeFromTokens(token) : util.createRange(0, 0, 0, Number.MAX_VALUE)
15✔
107
            });
108
        }
109

110
        const { prolog, root } = buildAST(cst as any, this.diagnostics);
220✔
111
        if (!root) {
220✔
112
            const token1 = tokenVector[0];
12✔
113
            const token2 = tokenVector[1];
12✔
114
            //whitespace before the prolog isn't allowed by the parser
115
            if (
12✔
116
                token1?.image.trim().length === 0 &&
50✔
117
                token2?.image.trim() === '<?xml'
6!
118
            ) {
119
                this.diagnostics.push({
2✔
120
                    ...DiagnosticMessages.xmlGenericParseError('Syntax error: whitespace found before the XML prolog'),
121
                    range: rangeFromTokens(token1)
122
                });
123
            }
124
        }
125

126
        if (isSGComponent(root)) {
220✔
127
            this.ast = new SGAst(prolog, root, root);
207✔
128
        } else {
129
            if (root) {
13✔
130
                //error: not a component
131
                this.diagnostics.push({
1✔
132
                    ...DiagnosticMessages.xmlUnexpectedTag(root.tag.text),
133
                    range: root.tag.range
134
                });
135
            }
136
            this.ast = new SGAst(prolog, root);
13✔
137
        }
138
    }
139
}
140

141
function buildAST(cst: DocumentCstNode, diagnostics: Diagnostic[]) {
142
    const { prolog: cstProlog, element } = cst.children;
220✔
143

144
    let prolog: SGProlog;
145
    if (cstProlog?.[0]) {
220✔
146
        const ctx = cstProlog[0].children;
190✔
147
        prolog = new SGProlog(
190✔
148
            mapToken(ctx.XMLDeclOpen[0]),
149
            mapAttributes(ctx.attribute),
150
            rangeFromTokens(ctx.XMLDeclOpen[0], ctx.SPECIAL_CLOSE[0])
151
        );
152
    }
153

154
    let root: SGTag;
155
    if (element.length > 0 && element[0]?.children?.Name) {
220!
156
        root = mapElement(element[0], diagnostics);
208✔
157
    }
158

159
    return {
220✔
160
        prolog: prolog,
161
        root: root
162
    };
163
}
164

165
//not exposed by @xml-tools/parser
166
interface IToken {
167
    image: string;
168
    startOffset: number;
169
    startLine?: number;
170
    startColumn?: number;
171
    endOffset?: number;
172
    endLine?: number;
173
    endColumn?: number;
174
}
175

176
function mapElement({ children }: ElementCstNode, diagnostics: Diagnostic[]): SGTag {
177
    const nameToken = children.Name[0];
440✔
178
    let range: Range;
179
    const selfClosing = !!children.SLASH_CLOSE;
440✔
180
    if (selfClosing) {
440✔
181
        const scToken = children.SLASH_CLOSE[0];
208✔
182
        range = rangeFromTokens(nameToken, scToken);
208✔
183
    } else {
184
        const endToken = children.END?.[0];
232!
185
        range = rangeFromTokens(nameToken, endToken);
232✔
186
    }
187
    const name = mapToken(nameToken);
440✔
188
    const attributes = mapAttributes(children.attribute);
440✔
189
    const content = children.content?.[0];
440✔
190
    switch (name.text) {
440✔
191
        case 'component':
440✔
192
            const componentContent = mapElements(content, ['interface', 'script', 'children', 'customization'], diagnostics);
207✔
193
            return new SGComponent(name, attributes, componentContent, range);
207✔
194
        case 'interface':
195
            const interfaceContent = mapElements(content, ['field', 'function'], diagnostics);
20✔
196
            return new SGInterface(name, interfaceContent, range);
20✔
197
        case 'field':
198
            if (hasElements(content)) {
11✔
199
                reportUnexpectedChildren(name, diagnostics);
1✔
200
            }
201
            return new SGField(name, attributes, range);
11✔
202
        case 'function':
203
            if (hasElements(content)) {
24!
UNCOV
204
                reportUnexpectedChildren(name, diagnostics);
×
205
            }
206
            return new SGFunction(name, attributes, range);
24✔
207
        case 'script':
208
            if (hasElements(content)) {
169✔
209
                reportUnexpectedChildren(name, diagnostics);
1✔
210
            }
211
            const cdata = getCdata(content);
169✔
212
            return new SGScript(name, attributes, cdata, range);
169✔
213
        case 'children':
214
            const childrenContent = mapNodes(content);
7✔
215
            return new SGChildren(name, childrenContent, range);
7✔
216
        default:
217
            const nodeContent = mapNodes(content);
2✔
218
            return new SGNode(name, attributes, nodeContent, range);
2✔
219
    }
220
}
221

222
function reportUnexpectedChildren(name: SGToken, diagnostics: Diagnostic[]) {
223
    diagnostics.push({
2✔
224
        ...DiagnosticMessages.xmlUnexpectedChildren(name.text),
225
        range: name.range
226
    });
227
}
228

229
function mapNode({ children }: ElementCstNode): SGNode {
230
    const nameToken = children.Name[0];
8✔
231
    let range: Range;
232
    const selfClosing = !!children.SLASH_CLOSE;
8✔
233
    if (selfClosing) {
8✔
234
        const scToken = children.SLASH_CLOSE[0];
7✔
235
        range = rangeFromTokens(nameToken, scToken);
7✔
236
    } else {
237
        const endToken = children.END?.[0];
1!
238
        range = rangeFromTokens(nameToken, endToken);
1✔
239
    }
240
    const name = mapToken(nameToken);
8✔
241
    const attributes = mapAttributes(children.attribute);
8✔
242
    const content = children.content?.[0];
8✔
243
    const nodeContent = mapNodes(content);
8✔
244
    return new SGNode(name, attributes, nodeContent, range);
8✔
245
}
246

247
function mapElements(content: ContentCstNode, allow: string[], diagnostics: Diagnostic[]): SGTag[] {
248
    if (!content) {
227✔
249
        return [];
5✔
250
    }
251
    const { element } = content.children;
222✔
252
    const tags: SGTag[] = [];
222✔
253
    if (element) {
222✔
254
        for (const entry of element) {
170✔
255
            const name = entry.children.Name?.[0];
237✔
256
            if (name?.image) {
237✔
257
                if (allow.includes(name.image)) {
236✔
258
                    tags.push(mapElement(entry, diagnostics));
232✔
259
                } else {
260
                    //unexpected tag
261
                    diagnostics.push({
4✔
262
                        ...DiagnosticMessages.xmlUnexpectedTag(name.image),
263
                        range: rangeFromTokens(name)
264
                    });
265
                }
266
            } else {
267
                //bad xml syntax...
268
            }
269
        }
270
    }
271
    return tags;
222✔
272
}
273

274
function mapNodes(content: ContentCstNode): SGNode[] {
275
    if (!content) {
17✔
276
        return [];
9✔
277
    }
278
    const { element } = content.children;
8✔
279
    return element?.map(element => mapNode(element));
8!
280
}
281

282
function hasElements(content: ContentCstNode): boolean {
283
    return !!content?.children.element?.length;
204✔
284
}
285

286
function getCdata(content: ContentCstNode): SGToken {
287
    if (!content) {
169✔
288
        return undefined;
167✔
289
    }
290
    const { CData } = content.children;
2✔
291
    return mapToken(CData?.[0]);
2✔
292
}
293

294
function mapToken(token: IToken, unquote = false): SGToken {
1,791✔
295
    if (!token) {
2,942✔
296
        return undefined;
3✔
297
    }
298
    return {
2,939✔
299
        text: unquote ? stripQuotes(token.image) : token.image,
2,939✔
300
        range: unquote ? rangeFromTokenValue(token) : rangeFromTokens(token)
2,939✔
301
    };
302
}
303

304
function mapAttributes(attributes: AttributeCstNode[]): SGAttribute[] {
305
    return attributes?.map(({ children }) => {
638✔
306
        const key = children.Name[0];
1,151✔
307
        const value = children.STRING?.[0];
1,151✔
308

309
        let openQuote: SGToken;
310
        let closeQuote: SGToken;
311
        //capture the leading and trailing quote tokens
312
        const match = /^(["']).*?(["'])$/.exec(value?.image);
1,151✔
313
        if (match) {
1,151✔
314
            const range = rangeFromTokenValue(value);
1,149✔
315
            openQuote = {
1,149✔
316
                text: match[1],
317
                range: util.createRange(range.start.line, range.start.character, range.start.line, range.start.character + 1)
318
            };
319
            closeQuote = {
1,149✔
320
                text: match[1],
321
                range: util.createRange(range.end.line, range.end.character - 1, range.end.line, range.end.character)
322
            };
323
        }
324
        return {
1,151✔
325
            key: mapToken(key),
326
            openQuote: openQuote,
327
            value: mapToken(value, true),
328
            closeQuote: closeQuote,
329
            range: rangeFromTokens(key, value)
330
        };
331
    }) || [];
332
}
333

334
//make range from `start` to `end` tokens
335
function rangeFromTokens(start: IToken, end?: IToken) {
336
    if (!end) {
3,592✔
337
        end = start;
1,805✔
338
    }
339
    return util.createRange(
3,592✔
340
        start.startLine - 1,
341
        start.startColumn - 1,
342
        end.endLine - 1,
343
        end.endColumn
344
    );
345
}
346

347
//make range not including quotes
348
export function rangeFromTokenValue(token: IToken) {
1✔
349
    if (!token) {
2,305!
UNCOV
350
        return undefined;
×
351
    }
352
    return util.createRange(
2,305✔
353
        token.startLine - 1,
354
        token.startColumn,
355
        token.endLine - 1,
356
        token.endColumn - 1
357
    );
358
}
359

360
function stripQuotes(value: string) {
361
    if (value?.length > 1) {
1,149!
362
        return value.substr(1, value.length - 2);
1,149✔
363
    }
UNCOV
364
    return '';
×
365
}
366

367
function emptySGReferences(): SGReferences {
368
    return {
213✔
369
        scriptTagImports: []
370
    };
371
}
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