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

rokucommunity / brighterscript / #13043

06 Sep 2024 07:15PM UTC coverage: 86.506% (-0.02%) from 86.524%
#13043

push

web-flow
Merge fcc2585b8 into 43cbf8b72

10856 of 13340 branches covered (81.38%)

Branch coverage included in aggregate %.

213 of 223 new or added lines in 10 files covered. (95.52%)

169 existing lines in 7 files now uncovered.

12536 of 13701 relevant lines covered (91.5%)

27503.39 hits per line

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

89.06
/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

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

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

22
export class BrightScriptDocParser {
1✔
23

24
    public parseNode(node: AstNode) {
25
        const matchingLocations: Location[] = [];
28,768✔
26
        const result = this.parse(
28,768✔
27
            util.getNodeDocumentation(node, {
28
                prettyPrint: false,
29
                matchingLocations: matchingLocations
30
            }),
31
            matchingLocations);
32
        for (const tag of result.tags) {
28,768✔
33
            if ((tag as BrsDocWithType).typeExpression) {
224✔
34
                (tag as BrsDocWithType).typeExpression.symbolTable = node.getSymbolTable();
205✔
35
            }
36
        }
37
        return result;
28,768✔
38
    }
39

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

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

100
    private parseLine(line: string, location?: Location) {
101
        line = line.trim();
247✔
102
        const match = tagRegex.exec(line);
247✔
103
        if (!match) {
247!
NEW
104
            return;
×
105
        }
106
        const tagName = match[1];
247✔
107
        const detail = match[2] ?? '';
247✔
108

109
        switch (tagName) {
247✔
110
            case BrsDocTagKind.Param:
317✔
111
                return { ...this.parseParam(detail), location: location };
140✔
112
            case BrsDocTagKind.Return:
113
            case 'returns':
114
                return { ...this.parseReturn(detail), location: location };
76✔
115
            case BrsDocTagKind.Type:
116
                return { ...this.parseType(detail), location: location };
26✔
117
        }
118
        return {
5✔
119
            tagName: tagName,
120
            detail: detail,
121
            location: location
122
        };
123
    }
124

125
    private parseParam(detail: string): BrsDocParamTag {
126
        let type = '';
140✔
127
        let description = '';
140✔
128
        let optional = false;
140✔
129
        let paramName = '';
140✔
130
        let match = paramRegex.exec(detail);
140✔
131
        if (match) {
140!
132
            type = match[1] ?? '';
140✔
133
            paramName = match[2] ?? '';
140!
134
            description = match[3] ?? '';
140!
135
        } else {
NEW
136
            paramName = detail.trim();
×
137
        }
138
        if (paramName) {
140!
139
            optional = paramName.startsWith('[') && paramName.endsWith(']');
140✔
140
            paramName = paramName.replace(/\[|\]/g, '').trim();
140✔
141
        }
142
        return {
140✔
143
            tagName: BrsDocTagKind.Param,
144
            name: paramName,
145
            typeString: type,
146
            typeExpression: this.getTypeExpressionFromTypeString(type),
147
            description: description,
148
            optional: optional,
149
            detail: detail
150
        };
151
    }
152

153
    private parseReturn(detail: string): BrsDocWithDescription {
154
        let match = returnRegex.exec(detail);
76✔
155
        let type = '';
76✔
156
        let description = '';
76✔
157
        if (match) {
76!
158
            type = match[1] ?? '';
76✔
159
            description = match[2] ?? '';
76!
160
        }
161
        return {
76✔
162
            tagName: BrsDocTagKind.Return,
163
            typeString: type,
164
            typeExpression: this.getTypeExpressionFromTypeString(type),
165
            description: description,
166
            detail: detail
167
        };
168
    }
169

170
    private parseType(detail: string): BrsDocWithType {
171
        let match = typeTagRegex.exec(detail);
26✔
172
        let type = '';
26✔
173
        if (match) {
26!
174
            if (match[1]) {
26!
175
                type = match[1] ?? '';
26!
176
            }
177
        }
178
        return {
26✔
179
            tagName: BrsDocTagKind.Type,
180
            typeString: type,
181
            typeExpression: this.getTypeExpressionFromTypeString(type),
182
            detail: detail
183
        };
184
    }
185

186
    private getTypeExpressionFromTypeString(typeString: string) {
187
        if (!typeString) {
242✔
188
            return undefined;
26✔
189
        }
190
        let result: Expression;
191
        try {
216✔
192
            let { ast } = Parser.parse(typeString);
216✔
193
            if (isExpressionStatement(ast?.statements?.[0])) {
216!
194
                result = (ast.statements[0] as ExpressionStatement).expression;
216✔
195
            }
196
        } catch (e) {
197
            //ignore
198
        }
199
        return result;
216✔
200
    }
201
}
202

203
export class BrightScriptDoc {
1✔
204

205
    protected _description: string;
206

207
    public tags = [] as BrsDocTag[];
28,785✔
208

209
    constructor(
210
        public readonly documentation: string
28,785✔
211
    ) {
212
    }
213

214
    set description(value: string) {
215
        this._description = value;
813✔
216
    }
217

218
    get description() {
219
        const descTag = this.tags.find((tag) => {
11✔
220
            return tag.tagName === BrsDocTagKind.Description;
16✔
221
        });
222

223
        let result = this._description ?? '';
11!
224
        if (descTag) {
11✔
225
            const descTagDetail = descTag.detail;
2✔
226
            result = result ? result + '\n' + descTagDetail : descTagDetail;
2!
227
        }
228
        return result.trim();
11✔
229
    }
230

231
    getParam(name: string) {
232
        const lowerName = name.toLowerCase();
5,164✔
233
        return this.tags.find((tag) => {
5,164✔
234
            return tag.tagName === BrsDocTagKind.Param && (tag as BrsDocParamTag).name.toLowerCase() === lowerName;
174✔
235
        }) as BrsDocParamTag;
236
    }
237

238
    getReturn() {
239
        return this.tags.find((tag) => {
3,798✔
240
            return tag.tagName === BrsDocTagKind.Return || tag.tagName === 'returns';
79✔
241
        }) as BrsDocWithDescription;
242
    }
243

244
    getTypeTag() {
245
        return this.tags.find((tag) => {
1,266✔
246
            return tag.tagName === BrsDocTagKind.Type;
10✔
247
        }) as BrsDocWithType;
248
    }
249

250
    getTypeTagByName(name: string) {
NEW
251
        const lowerName = name.toLowerCase();
×
NEW
252
        return this.tags.find((tag) => {
×
NEW
253
            return tag.tagName === BrsDocTagKind.Type && (tag as BrsDocParamTag).name.toLowerCase() === lowerName;
×
254
        }) as BrsDocWithType;
255
    }
256

257
    getTag(tagName: string) {
258
        return this.tags.find((tag) => {
3✔
259
            return tag.tagName === tagName;
3✔
260
        });
261
    }
262

263
    getAllTags(tagName: string) {
264
        return this.tags.filter((tag) => {
6✔
265
            return tag.tagName === tagName;
23✔
266
        });
267
    }
268

269
    getParamBscType(name: string, options: GetSymbolTypeOptions = { flags: SymbolTypeFlag.typetime }) {
×
270
        const param = this.getParam(name);
5,136✔
271
        return param?.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime });
5,136✔
272
    }
273

274
    getReturnBscType(options: GetSymbolTypeOptions = { flags: SymbolTypeFlag.typetime }) {
×
275
        const retTag = this.getReturn();
3,783✔
276
        return retTag?.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime });
3,783✔
277
    }
278

279
    getTypeTagBscType(options: GetSymbolTypeOptions = { flags: SymbolTypeFlag.typetime }) {
×
280
        const typeTag = this.getTypeTag();
1,264✔
281
        return typeTag?.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime });
1,264✔
282
    }
283
}
284

285
export interface BrsDocTag {
286
    tagName: string;
287
    detail?: string;
288
    location?: Location;
289
}
290
export interface BrsDocWithType extends BrsDocTag {
291
    typeString?: string;
292
    typeExpression?: Expression;
293
}
294

295
export interface BrsDocWithDescription extends BrsDocWithType {
296
    description?: string;
297
}
298

299
export interface BrsDocParamTag extends BrsDocWithDescription {
300
    name: string;
301
    optional?: boolean;
302
}
303

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

© 2026 Coveralls, Inc