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

rokucommunity / brighterscript / 37476610374

06 Oct 2026 02:09PM UTC coverage: 87.538%. First build
37476610374

Pull #1845

github

web-flow
Merge 216a691a5 into 8a04a9808
Pull Request #1845: Flatten token/node locations into pos/end/source

17131 of 20603 branches covered (83.15%)

Branch coverage included in aggregate %.

351 of 364 new or added lines in 30 files covered. (96.43%)

17885 of 19398 relevant lines covered (92.2%)

29789.07 hits per line

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

89.05
/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[] = [];
82,117 ✔
27
        const result = this.parse(
82,117 ✔
28
            util.getNodeDocumentation(node, {
29
                prettyPrint: false,
30
                commentTokens: commentTokens
31
            }),
32
            commentTokens);
33
        for (const tag of result.tags) {
82,117 ✔
34
            if ((tag as BrsDocWithType).typeExpression) {
483 ✔
35
                (tag as BrsDocWithType).typeExpression.symbolTable = node.getSymbolTable();
428 ✔
36
            }
37
        }
38
        return result;
82,117 ✔
39
    }
40

41
    public parse(documentation: string, matchingTokens: Token[] = []) {
17 ✔
42
        const brsDoc = new BrightScriptDoc(documentation);
82,134 ✔
43
        if (!documentation) {
82,134 ✔
44
            return brsDoc;
80,903 ✔
45
        }
46
        const lines = documentation.split('\n');
1,231 ✔
47
        const blockLines = [] as { line: string; token?: Token }[];
1,231 ✔
48
        const descriptionLines = [] as { line: string; token?: Token }[];
1,231 ✔
49
        let lastTag: BrsDocTag;
50
        let haveMatchingTokens = false;
1,231 ✔
51
        if (lines.length === matchingTokens.length) {
1,231 ✔
52
            // We locations for each line, so we can add Locations
53
            haveMatchingTokens = true;
1,214 ✔
54
        }
55
        function augmentLastTagWithBlockLines() {
56
            if (blockLines.length > 0 && lastTag) {
1,737 ✔
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,737 ✔
71
        }
72
        for (let line of lines) {
1,231 ✔
73
            let token = haveMatchingTokens ? matchingTokens.shift() : undefined;
1,557 ✔
74
            line = line.trim();
1,557 ✔
75
            while (line.startsWith('\'')) {
1,557 ✔
76
                // remove leading apostrophes
77
                line = line.substring(1).trim();
2 ✔
78
            }
79
            if (!line.startsWith('@')) {
1,557 ✔
80
                if (lastTag) {
1,051 ✔
81

82
                    blockLines.push({ line: line, token: token });
21 ✔
83
                } else if (descriptionLines.length > 0 || line) {
1,030 ✔
84
                    // add a line to the list if it's not empty
85
                    descriptionLines.push({ line: line, token: token });
1,014 ✔
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,231 ✔
97
        brsDoc.description = descriptionLines.map(obj => obj.line).join('\n').trim();
1,231 ✔
98
        return brsDoc;
1,231 ✔
99
    }
100

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

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

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

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

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

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

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

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

236
export class BrightScriptDoc {
1 ✔
237

238
    protected _description: string;
239

240
    public tags = [] as BrsDocTag[];
82,134 ✔
241

242
    constructor(
243
        public readonly documentation: string
82,134 ✔
244
    ) {
245
    }
246

247
    set description(value: string) {
248
        this._description = value;
1,231 ✔
249
    }
250

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

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

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

271
    getReturn() {
272
        return this.tags.find((tag) => {
6,626 ✔
273
            return tag.tagName === BrsDocTagKind.Return || tag.tagName === 'returns';
138 ✔
274
        }) as BrsDocWithDescription;
275
    }
276

277
    getTypeTag() {
278
        return this.tags.find((tag) => {
19,211 ✔
279
            return tag.tagName === BrsDocTagKind.Type;
10 ✔
280
        }) as BrsDocWithType;
281
    }
282

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

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

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

302
    getParamBscType(name: string, options: GetSymbolTypeOptions = { flags: SymbolTypeFlag.typetime }) {
×
303
        const param = this.getParam(name);
8,622 ✔
304
        return param?.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime });
8,622 ✔
305
    }
306

307
    getReturnBscType(options: GetSymbolTypeOptions = { flags: SymbolTypeFlag.typetime }) {
×
308
        const retTag = this.getReturn();
6,611 ✔
309
        return retTag?.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime });
6,611 ✔
310
    }
311

312
    getTypeTagBscType(options: GetSymbolTypeOptions = { flags: SymbolTypeFlag.typetime }) {
×
313
        const typeTag = this.getTypeTag();
19,209 ✔
314
        return typeTag?.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime });
19,209 ✔
315
    }
316
}
317

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

329
export interface BrsDocWithDescription extends BrsDocWithType {
330
    description?: string;
331
}
332

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

338
export let brsDocParser = new BrightScriptDocParser();
1 ✔
339
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