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

rokucommunity / brighterscript / 38934461267

16 Oct 2026 04:45PM UTC coverage: 88.175%. First build
38934461267

Pull #1845

github

web-flow
Merge 3ac5f4d76 into 79f2aa679
Pull Request #1845: Refactor Location tracking to be an offset-based Locatable model

18379 of 21907 branches covered (83.9%)

Branch coverage included in aggregate %.

517 of 529 new or added lines in 33 files covered. (97.73%)

19234 of 20750 relevant lines covered (92.69%)

578260.53 hits per line

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

89.12
/src/parser/BrightScriptDocParser.ts
1
import type { GetSymbolTypeOptions } from '../SymbolTable';
2
import util from '../util';
1 ✔
3
import type { AstNode, Expression } from './AstNode';
4
import type { Location } from 'vscode-languageserver';
5
import { Parser } from './Parser';
1 ✔
6
import type { ExpressionStatement } from './Statement';
7
import { isExpressionStatement } from '../astUtils/reflection';
1 ✔
8
import { SymbolTypeFlag } from '../SymbolTypeFlag';
1 ✔
9
import type { Token } from '../lexer/Token';
10

11
const tagRegex = /@(\w+)(?:\s+(.*))?/;
1 ✔
12
const paramRegex = /(?:{([^}]*)}\s+)?(?:(\[?\w+\]?))\s*(.*)/;
1 ✔
13
const returnRegex = /(?:{([^}]*)})?\s*(.*)/;
1 ✔
14
const typeTagRegex = /(?:{([^}]*)})?/;
1 ✔
15

16
export enum BrsDocTagKind {
1 ✔
17
    Description = 'description',
1 ✔
18
    Param = 'param',
1 ✔
19
    Return = 'return',
1 ✔
20
    Type = 'type'
1 ✔
21
}
22

23
export class BrightScriptDocParser {
1 ✔
24

25
    public parseNode(node: AstNode) {
26
        const commentTokens: Token[] = [];
21,879,428 ✔
27
        const result = this.parse(
21,879,428 ✔
28
            util.getNodeDocumentation(node, {
29
                prettyPrint: false,
30
                commentTokens: commentTokens
31
            }),
32
            commentTokens);
33
        for (const tag of result.tags) {
21,879,428 ✔
34
            if ((tag as BrsDocWithType).typeExpression) {
483 ✔
35
                (tag as BrsDocWithType).typeExpression.symbolTable = node.getSymbolTable();
428 ✔
36
            }
37
        }
38
        return result;
21,879,428 ✔
39
    }
40

41
    public parse(documentation: string, matchingTokens: Token[] = []) {
17 ✔
42
        const brsDoc = new BrightScriptDoc(documentation);
21,879,445 ✔
43
        if (!documentation) {
21,879,445 ✔
44
            return brsDoc;
21,878,178 ✔
45
        }
46
        const lines = documentation.split('\n');
1,267 ✔
47
        const blockLines = [] as { line: string; token?: Token }[];
1,267 ✔
48
        const descriptionLines = [] as { line: string; token?: Token }[];
1,267 ✔
49
        let lastTag: BrsDocTag;
50
        let haveMatchingTokens = false;
1,267 ✔
51
        if (lines.length === matchingTokens.length) {
1,267 ✔
52
            // We locations for each line, so we can add Locations
53
            haveMatchingTokens = true;
1,250 ✔
54
        }
55
        function augmentLastTagWithBlockLines() {
56
            if (blockLines.length > 0 && lastTag) {
1,773 ✔
57
                // add to the description or details to previous tag
58
                if (typeof (lastTag as BrsDocWithDescription).description !== 'undefined') {
17 ✔
59
                    (lastTag as BrsDocWithDescription).description += '\n' + blockLines.map(obj => obj.line).join('\n');
13 ✔
60
                    (lastTag as BrsDocWithDescription).description = (lastTag as BrsDocWithDescription).description.trim();
10 ✔
61
                }
62
                if (typeof lastTag.detail !== 'undefined') {
17 !
63
                    lastTag.detail += '\n' + blockLines.map(obj => obj.line).join('\n');
21 ✔
64
                    lastTag.detail = lastTag.detail.trim();
17 ✔
65
                }
66
                if (haveMatchingTokens) {
17 !
NEW
67
                    lastTag.location = util.createBoundingLocation(lastTag.location, util.getLocation(blockLines[blockLines.length - 1].token));
×
68
                }
69
            }
70
            blockLines.length = 0;
1,773 ✔
71
        }
72
        for (let line of lines) {
1,267 ✔
73
            let token = haveMatchingTokens ? matchingTokens.shift() : undefined;
1,599 ✔
74
            line = line.trim();
1,599 ✔
75
            while (line.startsWith('\'')) {
1,599 ✔
76
                // remove leading apostrophes
77
                line = line.substring(1).trim();
2 ✔
78
            }
79
            if (!line.startsWith('@')) {
1,599 ✔
80
                if (lastTag) {
1,093 ✔
81

82
                    blockLines.push({ line: line, token: token });
21 ✔
83
                } else if (descriptionLines.length > 0 || line) {
1,072 ✔
84
                    // add a line to the list if it's not empty
85
                    descriptionLines.push({ line: line, token: token });
1,056 ✔
86
                }
87
            } else {
88
                augmentLastTagWithBlockLines();
506 ✔
89
                const newTag = this.parseLine(line, token);
506 ✔
90
                lastTag = newTag;
506 ✔
91
                if (newTag) {
506 !
92
                    brsDoc.tags.push(newTag);
506 ✔
93
                }
94
            }
95
        }
96
        augmentLastTagWithBlockLines();
1,267 ✔
97
        brsDoc.description = descriptionLines.map(obj => obj.line).join('\n').trim();
1,267 ✔
98
        return brsDoc;
1,267 ✔
99
    }
100

101
    public getTypeLocationFromToken(token: Token): Location {
102
        const location = util.getLocation(token);
15 ✔
103
        if (!location) {
15 !
104
            return undefined;
×
105
        }
106
        const startCurly = token.text.indexOf('{');
15 ✔
107
        const endCurly = token.text.indexOf('}');
15 ✔
108
        if (startCurly === -1 || endCurly === -1 || endCurly <= startCurly) {
15 ✔
109
            return undefined;
6 ✔
110
        }
111
        const start = location.range.start;
9 ✔
112
        return {
9 ✔
113
            uri: location.uri,
114
            range: {
115
                start: {
116
                    line: start.line,
117
                    character: start.character + startCurly + 1
118
                },
119
                end: {
120
                    line: start.line,
121
                    character: start.character + endCurly
122
                }
123
            }
124
        };
125
    }
126

127
    private parseLine(line: string, token?: Token) {
128
        line = line.trim();
506 ✔
129
        const match = tagRegex.exec(line);
506 ✔
130
        if (!match) {
506 !
131
            return;
×
132
        }
133
        const tagName = match[1];
506 ✔
134
        const detail = match[2] ?? '';
506 ✔
135

136
        let result: BrsDocTag = {
506 ✔
137
            tagName: tagName,
138
            detail: detail
139
        };
140

141
        switch (tagName) {
506 ✔
142
            case BrsDocTagKind.Param:
629 ✔
143
                result = this.parseParam(detail);
322 ✔
144
                break;
322 ✔
145
            case BrsDocTagKind.Return:
146
            case 'returns':
147
                result = this.parseReturn(detail);
141 ✔
148
                break;
141 ✔
149
            case BrsDocTagKind.Type:
150
                result = this.parseType(detail);
26 ✔
151
                break;
26 ✔
152
        }
153
        return {
506 ✔
154
            ...result,
155
            token: token,
156
            location: util.getLocation(token)
157
        };
158
    }
159

160
    private parseParam(detail: string): BrsDocParamTag {
161
        let type = '';
322 ✔
162
        let description = '';
322 ✔
163
        let optional = false;
322 ✔
164
        let paramName = '';
322 ✔
165
        let match = paramRegex.exec(detail);
322 ✔
166
        if (match) {
322 !
167
            type = match[1] ?? '';
322 ✔
168
            paramName = match[2] ?? '';
322 !
169
            description = match[3] ?? '';
322 !
170
        } else {
171
            paramName = detail.trim();
×
172
        }
173
        if (paramName) {
322 !
174
            optional = paramName.startsWith('[') && paramName.endsWith(']');
322 ✔
175
            paramName = paramName.replace(/\[|\]/g, '').trim();
322 ✔
176
        }
177
        return {
322 ✔
178
            tagName: BrsDocTagKind.Param,
179
            name: paramName,
180
            typeString: type,
181
            typeExpression: this.getTypeExpressionFromTypeString(type),
182
            description: description,
183
            optional: optional,
184
            detail: detail
185
        };
186
    }
187

188
    private parseReturn(detail: string): BrsDocWithDescription {
189
        let match = returnRegex.exec(detail);
141 ✔
190
        let type = '';
141 ✔
191
        let description = '';
141 ✔
192
        if (match) {
141 !
193
            type = match[1] ?? '';
141 ✔
194
            description = match[2] ?? '';
141 !
195
        }
196
        return {
141 ✔
197
            tagName: BrsDocTagKind.Return,
198
            typeString: type,
199
            typeExpression: this.getTypeExpressionFromTypeString(type),
200
            description: description,
201
            detail: detail
202
        };
203
    }
204

205
    private parseType(detail: string): BrsDocWithType {
206
        let match = typeTagRegex.exec(detail);
26 ✔
207
        let type = '';
26 ✔
208
        if (match) {
26 !
209
            if (match[1]) {
26 !
210
                type = match[1] ?? '';
26 !
211
            }
212
        }
213
        return {
26 ✔
214
            tagName: BrsDocTagKind.Type,
215
            typeString: type,
216
            typeExpression: this.getTypeExpressionFromTypeString(type),
217
            detail: detail
218
        };
219
    }
220

221
    private getTypeExpressionFromTypeString(typeString: string) {
222
        if (!typeString) {
489 ✔
223
            return undefined;
50 ✔
224
        }
225
        let result: Expression;
226
        try {
439 ✔
227
            let { ast } = Parser.parse(typeString);
439 ✔
228
            if (isExpressionStatement(ast?.statements?.[0])) {
439 !
229
                result = (ast.statements[0] as ExpressionStatement).expression;
439 ✔
230
            }
231
        } catch (e) {
232
            //ignore
233
        }
234
        return result;
439 ✔
235
    }
236
}
237

238
export class BrightScriptDoc {
1 ✔
239

240
    protected _description: string;
241

242
    public tags = [] as BrsDocTag[];
21,879,445 ✔
243

244
    constructor(
245
        public readonly documentation: string
21,879,445 ✔
246
    ) {
247
    }
248

249
    set description(value: string) {
250
        this._description = value;
1,267 ✔
251
    }
252

253
    get description() {
254
        const descTag = this.tags.find((tag) => {
11 ✔
255
            return tag.tagName === BrsDocTagKind.Description;
16 ✔
256
        });
257

258
        let result = this._description ?? '';
11 !
259
        if (descTag) {
11 ✔
260
            const descTagDetail = descTag.detail;
2 ✔
261
            result = result ? result + '\n' + descTagDetail : descTagDetail;
2 !
262
        }
263
        return result.trim();
11 ✔
264
    }
265

266
    getParam(name: string) {
267
        const lowerName = name.toLowerCase();
20,751 ✔
268
        return this.tags.find((tag) => {
20,751 ✔
269
            return tag.tagName === BrsDocTagKind.Param && (tag as BrsDocParamTag).name.toLowerCase() === lowerName;
454 ✔
270
        }) as BrsDocParamTag;
271
    }
272

273
    getReturn() {
274
        return this.tags.find((tag) => {
8,143 ✔
275
            return tag.tagName === BrsDocTagKind.Return || tag.tagName === 'returns';
138 ✔
276
        }) as BrsDocWithDescription;
277
    }
278

279
    getTypeTag() {
280
        return this.tags.find((tag) => {
10,899,497 ✔
281
            return tag.tagName === BrsDocTagKind.Type;
10 ✔
282
        }) as BrsDocWithType;
283
    }
284

285
    getTypeTagByName(name: string) {
286
        const lowerName = name.toLowerCase();
×
287
        return this.tags.find((tag) => {
×
288
            return tag.tagName === BrsDocTagKind.Type && (tag as BrsDocParamTag).name.toLowerCase() === lowerName;
×
289
        }) as BrsDocWithType;
290
    }
291

292
    getTag(tagName: string) {
293
        return this.tags.find((tag) => {
3 ✔
294
            return tag.tagName === tagName;
3 ✔
295
        });
296
    }
297

298
    getAllTags(tagName: string) {
299
        return this.tags.filter((tag) => {
6 ✔
300
            return tag.tagName === tagName;
23 ✔
301
        });
302
    }
303

304
    getParamBscType(name: string, options: GetSymbolTypeOptions = { flags: SymbolTypeFlag.typetime }) {
×
305
        const param = this.getParam(name);
10,363 ✔
306
        return param?.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime });
10,363 ✔
307
    }
308

309
    getReturnBscType(options: GetSymbolTypeOptions = { flags: SymbolTypeFlag.typetime }) {
×
310
        const retTag = this.getReturn();
8,128 ✔
311
        return retTag?.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime });
8,128 ✔
312
    }
313

314
    getTypeTagBscType(options: GetSymbolTypeOptions = { flags: SymbolTypeFlag.typetime }) {
×
315
        const typeTag = this.getTypeTag();
10,899,495 ✔
316
        return typeTag?.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime });
10,899,495 ✔
317
    }
318
}
319

320
export interface BrsDocTag {
321
    tagName: string;
322
    detail?: string;
323
    location?: Location;
324
    token?: Token;
325
}
326
export interface BrsDocWithType extends BrsDocTag {
327
    typeString?: string;
328
    typeExpression?: Expression;
329
}
330

331
export interface BrsDocWithDescription extends BrsDocWithType {
332
    description?: string;
333
}
334

335
export interface BrsDocParamTag extends BrsDocWithDescription {
336
    name: string;
337
    optional?: boolean;
338
}
339

340
export let brsDocParser = new BrightScriptDocParser();
1 ✔
341
export default brsDocParser;
1 ✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc