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

rokucommunity / brighterscript / 36323593387

27 Sep 2026 01:47PM UTC coverage: 87.512%. First build
36323593387

Pull #1845

github

web-flow
Merge 6efeff4e7 into 111a3d9c8
Pull Request #1845: Flatten token/node locations into pos/end/source

17009 of 20465 branches covered (83.11%)

Branch coverage included in aggregate %.

274 of 285 new or added lines in 30 files covered. (96.14%)

17778 of 19286 relevant lines covered (92.18%)

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

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

82
                    blockLines.push({ line: line, token: token });
21 ✔
83
                } else if (descriptionLines.length > 0 || line) {
975 ✔
84
                    // add a line to the list if it's not empty
85
                    descriptionLines.push({ line: line, token: token });
959 ✔
86
                }
87
            } else {
88
                augmentLastTagWithBlockLines();
353 ✔
89
                const newTag = this.parseLine(line, token);
353 ✔
90
                lastTag = newTag;
353 ✔
91
                if (newTag) {
353 !
92
                    brsDoc.tags.push(newTag);
353 ✔
93
                }
94
            }
95
        }
96
        augmentLastTagWithBlockLines();
1,157 ✔
97
        brsDoc.description = descriptionLines.map(obj => obj.line).join('\n').trim();
1,157 ✔
98
        return brsDoc;
1,157 ✔
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();
353 ✔
127
        const match = tagRegex.exec(line);
353 ✔
128
        if (!match) {
353 !
129
            return;
×
130
        }
131
        const tagName = match[1];
353 ✔
132
        const detail = match[2] ?? '';
353 ✔
133

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

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

158
    private parseParam(detail: string): BrsDocParamTag {
159
        let type = '';
222 ✔
160
        let description = '';
222 ✔
161
        let optional = false;
222 ✔
162
        let paramName = '';
222 ✔
163
        let match = paramRegex.exec(detail);
222 ✔
164
        if (match) {
222 !
165
            type = match[1] ?? '';
222 ✔
166
            paramName = match[2] ?? '';
222 !
167
            description = match[3] ?? '';
222 !
168
        } else {
169
            paramName = detail.trim();
×
170
        }
171
        if (paramName) {
222 !
172
            optional = paramName.startsWith('[') && paramName.endsWith(']');
222 ✔
173
            paramName = paramName.replace(/\[|\]/g, '').trim();
222 ✔
174
        }
175
        return {
222 ✔
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);
88 ✔
188
        let type = '';
88 ✔
189
        let description = '';
88 ✔
190
        if (match) {
88 !
191
            type = match[1] ?? '';
88 ✔
192
            description = match[2] ?? '';
88 !
193
        }
194
        return {
88 ✔
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) {
336 ✔
221
            return undefined;
36 ✔
222
        }
223
        let result: Expression;
224
        try {
300 ✔
225
            let { ast } = Parser.parse(typeString);
300 ✔
226
            if (isExpressionStatement(ast?.statements?.[0])) {
300 !
227
                result = (ast.statements[0] as ExpressionStatement).expression;
300 ✔
228
            }
229
        } catch (e) {
230
            //ignore
231
        }
232
        return result;
300 ✔
233
    }
234
}
235

236
export class BrightScriptDoc {
1 ✔
237

238
    protected _description: string;
239

240
    public tags = [] as BrsDocTag[];
45,720 ✔
241

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

247
    set description(value: string) {
248
        this._description = value;
1,157 ✔
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();
15,603 ✔
266
        return this.tags.find((tag) => {
15,603 ✔
267
            return tag.tagName === BrsDocTagKind.Param && (tag as BrsDocParamTag).name.toLowerCase() === lowerName;
354 ✔
268
        }) as BrsDocParamTag;
269
    }
270

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

277
    getTypeTag() {
278
        return this.tags.find((tag) => {
2,263 ✔
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);
7,789 ✔
304
        return param?.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime });
7,789 ✔
305
    }
306

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

312
    getTypeTagBscType(options: GetSymbolTypeOptions = { flags: SymbolTypeFlag.typetime }) {
×
313
        const typeTag = this.getTypeTag();
2,261 ✔
314
        return typeTag?.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime });
2,261 ✔
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