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

rokucommunity / brighterscript / #13231

24 Oct 2024 01:02PM UTC coverage: 86.866% (-1.3%) from 88.214%
#13231

push

web-flow
Merge cc3491b40 into 7cfaaa047

11613 of 14131 branches covered (82.18%)

Branch coverage included in aggregate %.

7028 of 7618 new or added lines in 100 files covered. (92.26%)

87 existing lines in 18 files now uncovered.

12732 of 13895 relevant lines covered (91.63%)

30018.29 hits per line

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

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

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

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

101
    public getTypeLocationFromToken(token: Token): Location {
102
        if (!token?.location) {
15!
NEW
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: token.location.uri,
112
            range: {
113
                start: {
114
                    line: token.location.range.start.line,
115
                    character: token.location.range.start.character + startCurly + 1
116
                },
117
                end: {
118
                    line: token.location.range.start.line,
119
                    character: token.location.range.start.character + endCurly
120
                }
121
            }
122
        };
123
    }
124

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

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

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

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

236
export class BrightScriptDoc {
1✔
237

238
    protected _description: string;
239

240
    public tags = [] as BrsDocTag[];
29,611✔
241

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

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

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

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

283
    getTypeTagByName(name: string) {
NEW
284
        const lowerName = name.toLowerCase();
×
NEW
285
        return this.tags.find((tag) => {
×
NEW
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);
5,248✔
304
        return param?.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime });
5,248✔
305
    }
306

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

312
    getTypeTagBscType(options: GetSymbolTypeOptions = { flags: SymbolTypeFlag.typetime }) {
×
313
        const typeTag = this.getTypeTag();
1,288✔
314
        return typeTag?.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime });
1,288✔
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 · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc