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

rokucommunity / brighterscript / #15024

13 Dec 2025 02:11AM UTC coverage: 87.29% (-0.5%) from 87.825%
#15024

push

web-flow
Merge d8dcd8d52 into a65ebfcad

14406 of 17439 branches covered (82.61%)

Branch coverage included in aggregate %.

36 of 36 new or added lines in 4 files covered. (100.0%)

909 existing lines in 49 files now uncovered.

15091 of 16353 relevant lines covered (92.28%)

24217.12 hits per line

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

78.24
/src/bscPlugin/codeActions/CodeActionsProcessor.ts
1
import { CodeActionKind } from 'vscode-languageserver';
1✔
2
import { codeActionUtil } from '../../CodeActionUtil';
1✔
3
import type { DiagnosticMessageType } from '../../DiagnosticMessages';
4
import { DiagnosticCodeMap } from '../../DiagnosticMessages';
1✔
5
import type { BrsFile } from '../../files/BrsFile';
6
import type { BscFile } from '../../files/BscFile';
7
import type { XmlFile } from '../../files/XmlFile';
8
import type { BsDiagnostic, OnGetCodeActionsEvent } from '../../interfaces';
9
import { ParseMode } from '../../parser/Parser';
1✔
10
import { util } from '../../util';
1✔
11
import { isBrsFile, isFunctionExpression, isVariableExpression, isVoidType } from '../../astUtils/reflection';
1✔
12
import type { FunctionExpression } from '../../parser/Expression';
13
import { TokenKind } from '../../lexer/TokenKind';
1✔
14
import { SymbolTypeFlag } from '../../SymbolTypeFlag';
1✔
15

16
export class CodeActionsProcessor {
1✔
17
    public constructor(
18
        public event: OnGetCodeActionsEvent
15✔
19
    ) {
20

21
    }
22

23
    public process() {
24
        for (const diagnostic of this.event.diagnostics) {
15✔
25
            if (diagnostic.code === DiagnosticCodeMap.cannotFindName || diagnostic.code === DiagnosticCodeMap.cannotFindFunction) {
19✔
26
                this.suggestCannotFindName(diagnostic as any);
8✔
27
            } else if (diagnostic.code === DiagnosticCodeMap.xmlComponentMissingExtendsAttribute) {
11✔
28
                this.addMissingExtends(diagnostic as any);
2✔
29
            } else if (diagnostic.code === DiagnosticCodeMap.voidFunctionMayNotReturnValue) {
9✔
30
                this.addVoidFunctionReturnActions(diagnostic);
2✔
31
            } else if (diagnostic.code === DiagnosticCodeMap.nonVoidFunctionMustReturnValue) {
7✔
32
                this.addNonVoidFunctionReturnActions(diagnostic);
2✔
33
            }
34
        }
35
    }
36

37
    private suggestedImports = new Set<string>();
15✔
38

39
    /**
40
     * Generic import suggestion function. Shouldn't be called directly from the main loop, but instead called by more specific diagnostic handlers
41
     */
42
    private suggestImports(diagnostic: BsDiagnostic, key: string, files: BscFile[]) {
43
        //skip if we already have this suggestion
44
        if (this.suggestedImports.has(key) || !isBrsFile(this.event.file)) {
7!
UNCOV
45
            return;
×
46
        }
47

48
        this.suggestedImports.add(key);
7✔
49
        // eslint-disable-next-line @typescript-eslint/dot-notation
50
        const importStatements = this.event.file['_cachedLookups'].importStatements;
7✔
51
        //find the position of the first import statement, or the top of the file if there is none
52
        const insertPosition = importStatements[importStatements.length - 1]?.tokens.import?.location?.range?.start ?? util.createPosition(0, 0);
7✔
53

54
        //find all files that reference this function
55
        for (const file of files) {
7✔
56
            const destPath = util.sanitizePkgPath(file.destPath);
9✔
57
            this.event.codeActions.push(
9✔
58
                codeActionUtil.createCodeAction({
59
                    title: `import "${destPath}"`,
60
                    diagnostics: [diagnostic],
61
                    isPreferred: false,
62
                    kind: CodeActionKind.QuickFix,
63
                    changes: [{
64
                        type: 'insert',
65
                        filePath: this.event.file.srcPath,
66
                        position: insertPosition,
67
                        newText: `import "${destPath}"\n`
68
                    }]
69
                })
70
            );
71
        }
72
    }
73

74
    private suggestCannotFindName(diagnostic: DiagnosticMessageType<'cannotFindName'>) {
75
        //skip if not a BrighterScript file
76
        const file = this.event.program.getFile(diagnostic.location?.uri);
8!
77
        if (!file || (file as BrsFile).parseMode !== ParseMode.BrighterScript) {
8✔
78
            return;
1✔
79
        }
80
        const lowerName = (diagnostic.data.fullName ?? diagnostic.data.name).toLowerCase();
7!
81

82
        this.suggestImports(
7✔
83
            diagnostic,
84
            lowerName,
85
            [
86
                ...this.event.program.findFilesForFunction(lowerName),
87
                ...this.event.program.findFilesForClass(lowerName),
88
                ...this.event.program.findFilesForNamespace(lowerName),
89
                ...this.event.program.findFilesForEnum(lowerName)
90
            ]
91
        );
92
    }
93

94
    private addMissingExtends(diagnostic: DiagnosticMessageType<'xmlComponentMissingExtendsAttribute'>) {
95
        const srcPath = this.event.file.srcPath;
2✔
96
        const { componentElement } = (this.event.file as XmlFile).parser.ast;
2✔
97
        //inject new attribute after the final attribute, or after the `<component` if there are no attributes
98
        const pos = (componentElement.attributes[componentElement.attributes.length - 1] ?? componentElement.tokens.startTagName)?.location?.range.end;
2!
99
        this.event.codeActions.push(
2✔
100
            codeActionUtil.createCodeAction({
101
                title: `Extend "Group"`,
102
                diagnostics: [diagnostic],
103
                isPreferred: true,
104
                kind: CodeActionKind.QuickFix,
105
                changes: [{
106
                    type: 'insert',
107
                    filePath: srcPath,
108
                    position: pos,
109
                    newText: ' extends="Group"'
110
                }]
111
            })
112
        );
113
        this.event.codeActions.push(
2✔
114
            codeActionUtil.createCodeAction({
115
                title: `Extend "Task"`,
116
                diagnostics: [diagnostic],
117
                kind: CodeActionKind.QuickFix,
118
                changes: [{
119
                    type: 'insert',
120
                    filePath: srcPath,
121
                    position: pos,
122
                    newText: ' extends="Task"'
123
                }]
124
            })
125
        );
126
        this.event.codeActions.push(
2✔
127
            codeActionUtil.createCodeAction({
128
                title: `Extend "ContentNode"`,
129
                diagnostics: [diagnostic],
130
                kind: CodeActionKind.QuickFix,
131
                changes: [{
132
                    type: 'insert',
133
                    filePath: srcPath,
134
                    position: pos,
135
                    newText: ' extends="ContentNode"'
136
                }]
137
            })
138
        );
139
    }
140

141
    private addVoidFunctionReturnActions(diagnostic: BsDiagnostic) {
142
        this.event.codeActions.push(
2✔
143
            codeActionUtil.createCodeAction({
144
                title: `Remove return value`,
145
                diagnostics: [diagnostic],
146
                kind: CodeActionKind.QuickFix,
147
                changes: [{
148
                    type: 'delete',
149
                    filePath: this.event.file.srcPath,
150
                    range: util.createRange(
151
                        diagnostic.location.range.start.line,
152
                        diagnostic.location.range.start.character + 'return'.length,
153
                        diagnostic.location.range.end.line,
154
                        diagnostic.location.range.end.character
155
                    )
156
                }]
157
            })
158
        );
159
        if (isBrsFile(this.event.file)) {
2!
160
            const expression = this.event.file.getClosestExpression(diagnostic.location.range.start);
2✔
161
            const func = expression.findAncestor<FunctionExpression>(isFunctionExpression);
2✔
162

163
            //if we're in a sub and we do not have a return type, suggest converting to a function
164
            if (func.tokens.functionType.kind === TokenKind.Sub && !func.returnTypeExpression) {
2✔
165
                //find the first function in a file that uses the `function` keyword
166
                const referenceFunction = this.event.file.parser.ast.findChild<FunctionExpression>((node) => {
1✔
167
                    return isFunctionExpression(node) && node.tokens.functionType.kind === TokenKind.Function;
5✔
168
                });
169
                const functionTypeText = referenceFunction?.tokens.functionType.text ?? 'function';
1!
170
                const endFunctionTypeText = referenceFunction?.tokens.endFunctionType?.text ?? 'end function';
1!
171
                this.event.codeActions.push(
1✔
172
                    codeActionUtil.createCodeAction({
173
                        title: `Convert ${func.tokens.functionType.text} to ${functionTypeText}`,
174
                        diagnostics: [diagnostic],
175
                        kind: CodeActionKind.QuickFix,
176
                        changes: [
177
                            //function
178
                            {
179
                                type: 'replace',
180
                                filePath: this.event.file.srcPath,
181
                                range: func.tokens.functionType.location.range,
182
                                newText: functionTypeText
183
                            },
184
                            //end function
185
                            {
186
                                type: 'replace',
187
                                filePath: this.event.file.srcPath,
188
                                range: func.tokens.endFunctionType.location.range,
189
                                newText: endFunctionTypeText
190
                            }
191
                        ]
192
                    })
193
                );
194
            }
195

196
            //function `as void` return type. Suggest removing the return type
197
            if (func.tokens.functionType.kind === TokenKind.Function && isVoidType(func.returnTypeExpression.getType({ flags: SymbolTypeFlag.typetime }))) {
2✔
198
                this.event.codeActions.push(
1✔
199
                    codeActionUtil.createCodeAction({
200
                        title: `Remove return type from function declaration`,
201
                        diagnostics: [diagnostic],
202
                        kind: CodeActionKind.QuickFix,
203
                        changes: [{
204
                            type: 'delete',
205
                            filePath: this.event.file.srcPath,
206
                            // )| as void|
207
                            range: util.createRange(
208
                                func.tokens.rightParen.location.range.start.line,
209
                                func.tokens.rightParen.location.range.start.character + 1,
210
                                func.returnTypeExpression.location.range.end.line,
211
                                func.returnTypeExpression.location.range.end.character
212
                            )
213
                        }]
214
                    })
215
                );
216
            }
217
        }
218
    }
219

220
    private addNonVoidFunctionReturnActions(diagnostic: BsDiagnostic) {
221
        if (isBrsFile(this.event.file)) {
2!
222
            const expression = this.event.file.getClosestExpression(diagnostic.location.range.start);
2✔
223
            const func = expression.findAncestor<FunctionExpression>(isFunctionExpression);
2✔
224

225
            //`sub as <non-void type>`, suggest removing the return type
226
            if (
2✔
227
                func.tokens.functionType.kind === TokenKind.Sub &&
4✔
228
                //has a return type
229
                func.returnTypeExpression &&
230
                //is not `as void`
231
                !(isVariableExpression(func.returnTypeExpression.expression) && func.returnTypeExpression.expression.tokens.name.text?.toLowerCase() === 'void')
5!
232
            ) {
233
                this.event.codeActions.push(
1✔
234
                    codeActionUtil.createCodeAction({
235
                        title: `Remove return type from sub declaration`,
236
                        diagnostics: [diagnostic],
237
                        kind: CodeActionKind.QuickFix,
238
                        changes: [{
239
                            type: 'delete',
240
                            filePath: this.event.file.srcPath,
241
                            // )| as void|
242
                            range: util.createRange(
243
                                func.tokens.rightParen.location.range.start.line,
244
                                func.tokens.rightParen.location.range.start.character + 1,
245
                                func.returnTypeExpression.location.range.end.line,
246
                                func.returnTypeExpression.location.range.end.character
247
                            )
248
                        }]
249
                    })
250
                );
251
            }
252

253
            //function with no return type.
254
            if (func.tokens.functionType.kind === TokenKind.Function && !func.returnTypeExpression) {
2✔
255
                //find tokens for `as` and `void` in the file if possible
256
                let asText: string;
257
                let voidText: string;
258
                let subText: string;
259
                let endSubText: string;
260
                for (const token of this.event.file.parser.tokens) {
1✔
261
                    if (asText && voidText && subText && endSubText) {
12!
UNCOV
262
                        break;
×
263
                    }
264
                    if (token?.kind === TokenKind.As) {
12!
UNCOV
265
                        asText = token?.text;
×
266
                    } else if (token?.kind === TokenKind.Void) {
12!
UNCOV
267
                        voidText = token?.text;
×
268
                    } else if (token?.kind === TokenKind.Sub) {
12!
UNCOV
269
                        subText = token?.text;
×
270
                    } else if (token?.kind === TokenKind.EndSub) {
12!
UNCOV
271
                        endSubText = token?.text;
×
272
                    }
273
                }
274

275
                //suggest converting to `as void`
276
                this.event.codeActions.push(
1✔
277
                    codeActionUtil.createCodeAction({
278
                        title: `Add void return type to function declaration`,
279
                        diagnostics: [diagnostic],
280
                        kind: CodeActionKind.QuickFix,
281
                        changes: [{
282
                            type: 'insert',
283
                            filePath: this.event.file.srcPath,
284
                            position: func.tokens.rightParen.location.range.end,
285
                            newText: ` ${asText ?? 'as'} ${voidText ?? 'void'}`
6!
286
                        }]
287
                    })
288
                );
289
                //suggest converting to sub
290
                this.event.codeActions.push(
1✔
291
                    codeActionUtil.createCodeAction({
292
                        title: `Convert function to sub`,
293
                        diagnostics: [diagnostic],
294
                        kind: CodeActionKind.QuickFix,
295
                        changes: [{
296
                            type: 'replace',
297
                            filePath: this.event.file.srcPath,
298
                            range: func.tokens.functionType.location.range,
299
                            newText: subText ?? 'sub'
3!
300
                        }, {
301
                            type: 'replace',
302
                            filePath: this.event.file.srcPath,
303
                            range: func.tokens.endFunctionType.location.range,
304
                            newText: endSubText ?? 'end sub'
3!
305
                        }]
306
                    })
307
                );
308
            }
309
        }
310
    }
311
}
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