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

rokucommunity / brighterscript / #15445

25 Mar 2026 07:04PM UTC coverage: 88.624% (-0.4%) from 88.992%
#15445

push

web-flow
Merge f83491a8a into adf045c2c

7960 of 9472 branches covered (84.04%)

Branch coverage included in aggregate %.

18 of 64 new or added lines in 9 files covered. (28.13%)

1 existing line in 1 file now uncovered.

10215 of 11036 relevant lines covered (92.56%)

1950.39 hits per line

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

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

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

21
    }
22

23
    public process() {
24
        for (const diagnostic of this.event.diagnostics) {
14✔
25
            if (diagnostic.code === DiagnosticCodeMap.cannotFindName || diagnostic.code === DiagnosticCodeMap.cannotFindFunction) {
14✔
26
                this.suggestCannotFindName(diagnostic as any);
8✔
27
            } else if (diagnostic.code === DiagnosticCodeMap.classCouldNotBeFound) {
6!
28
                this.suggestClassImports(diagnostic as any);
×
29
            } else if (diagnostic.code === DiagnosticCodeMap.xmlComponentMissingExtendsAttribute) {
6✔
30
                this.addMissingExtends(diagnostic as any);
2✔
31
            } else if (diagnostic.code === DiagnosticCodeMap.voidFunctionMayNotReturnValue) {
4✔
32
                this.addVoidFunctionReturnActions(diagnostic);
2✔
33
            } else if (diagnostic.code === DiagnosticCodeMap.nonVoidFunctionMustReturnValue) {
2!
34
                this.addNonVoidFunctionReturnActions(diagnostic);
2✔
35
            }
36
        }
37
    }
38

39
    private suggestedImports = new Set<string>();
14✔
40

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

50
        this.suggestedImports.add(key);
7✔
51
        const importStatements = (this.event.file as BrsFile).parser.references.importStatements;
7✔
52
        //find the position of the first import statement, or the top of the file if there is none
53
        const insertPosition = importStatements[importStatements.length - 1]?.importToken.range?.start ?? util.createPosition(0, 0);
7✔
54

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

75
    private suggestCannotFindName(diagnostic: DiagnosticMessageType<'cannotFindName'>) {
76
        //skip if not a BrighterScript file
77
        if ((diagnostic.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.file.program.findFilesForFunction(lowerName),
87
                ...this.event.file.program.findFilesForClass(lowerName),
88
                ...this.event.file.program.findFilesForNamespace(lowerName),
89
                ...this.event.file.program.findFilesForEnum(lowerName)
90
            ]
91
        );
92
    }
93

94
    private suggestClassImports(diagnostic: DiagnosticMessageType<'classCouldNotBeFound'>) {
95
        //skip if not a BrighterScript file
96
        if ((diagnostic.file as BrsFile).parseMode !== ParseMode.BrighterScript) {
×
97
            return;
×
98
        }
99
        const lowerClassName = diagnostic.data.className.toLowerCase();
×
100
        this.suggestImports(
×
101
            diagnostic,
102
            lowerClassName,
103
            this.event.file.program.findFilesForClass(lowerClassName)
104
        );
105
    }
106

107
    private addMissingExtends(diagnostic: DiagnosticMessageType<'xmlComponentMissingExtendsAttribute'>) {
108
        const srcPath = this.event.file.srcPath;
2✔
109
        const pos = getMissingExtendsInsertPosition(this.event.file as XmlFile);
2✔
110
        this.event.codeActions.push(
2✔
111
            codeActionUtil.createCodeAction({
112
                title: `Extend "Group"`,
113
                diagnostics: [diagnostic],
114
                isPreferred: true,
115
                kind: CodeActionKind.QuickFix,
116
                changes: [{
117
                    type: 'insert',
118
                    filePath: srcPath,
119
                    position: pos,
120
                    newText: ' extends="Group"'
121
                }]
122
            })
123
        );
124
        this.event.codeActions.push(
2✔
125
            codeActionUtil.createCodeAction({
126
                title: `Extend "Task"`,
127
                diagnostics: [diagnostic],
128
                kind: CodeActionKind.QuickFix,
129
                changes: [{
130
                    type: 'insert',
131
                    filePath: srcPath,
132
                    position: pos,
133
                    newText: ' extends="Task"'
134
                }]
135
            })
136
        );
137
        this.event.codeActions.push(
2✔
138
            codeActionUtil.createCodeAction({
139
                title: `Extend "ContentNode"`,
140
                diagnostics: [diagnostic],
141
                kind: CodeActionKind.QuickFix,
142
                changes: [{
143
                    type: 'insert',
144
                    filePath: srcPath,
145
                    position: pos,
146
                    newText: ' extends="ContentNode"'
147
                }]
148
            })
149
        );
150
    }
151

152
    private addVoidFunctionReturnActions(diagnostic: Diagnostic) {
153
        this.event.codeActions.push(
2✔
154
            codeActionUtil.createCodeAction({
155
                title: `Remove return value`,
156
                diagnostics: [diagnostic],
157
                kind: CodeActionKind.QuickFix,
158
                changes: [getRemoveReturnValueChange(diagnostic, this.event.file.srcPath)]
159
            })
160
        );
161

162
        // If there are multiple instances in this file, offer a "fix all" quickfix that appears
163
        // directly in the lightbulb (same as ESLint's "Fix all 'rule-name' problems" pattern)
164
        const allInFile = this.event.program.getDiagnostics()
2✔
165
            .filter(x => x.file === this.event.file && x.code === diagnostic.code);
2✔
166
        if (allInFile.length > 1) {
2!
NEW
167
            this.event.codeActions.push(
×
168
                codeActionUtil.createCodeAction({
169
                    title: `Fix all: Remove void return values`,
170
                    kind: CodeActionKind.QuickFix,
NEW
171
                    changes: allInFile.map(d => getRemoveReturnValueChange(d, this.event.file.srcPath))
×
172
                })
173
            );
174
        }
175
        if (isBrsFile(this.event.file)) {
2!
176
            const expression = this.event.file.getClosestExpression(diagnostic.range.start);
2✔
177
            const func = expression.findAncestor<FunctionExpression>(isFunctionExpression);
2✔
178

179
            //if we're in a sub and we do not have a return type, suggest converting to a function
180
            if (func.functionType.kind === TokenKind.Sub && !func.returnTypeToken) {
2✔
181
                //find the first function in a file that uses the `function` keyword
182
                const referenceFunction = this.event.file.parser.ast.findChild<FunctionExpression>((node) => {
1✔
183
                    return isFunctionExpression(node) && node.functionType.kind === TokenKind.Function;
6✔
184
                });
185
                const functionTypeText = referenceFunction?.functionType.text ?? 'function';
1!
186
                const endFunctionTypeText = referenceFunction?.end?.text ?? 'end function';
1!
187
                this.event.codeActions.push(
1✔
188
                    codeActionUtil.createCodeAction({
189
                        title: `Convert ${func.functionType.text} to ${functionTypeText}`,
190
                        diagnostics: [diagnostic],
191
                        kind: CodeActionKind.QuickFix,
192
                        changes: [
193
                            //function
194
                            {
195
                                type: 'replace',
196
                                filePath: this.event.file.srcPath,
197
                                range: func.functionType.range,
198
                                newText: functionTypeText
199
                            },
200
                            //end function
201
                            {
202
                                type: 'replace',
203
                                filePath: this.event.file.srcPath,
204
                                range: func.end.range,
205
                                newText: endFunctionTypeText
206
                            }
207
                        ]
208
                    })
209
                );
210
            }
211

212
            //function `as void` return type. Suggest removing the return type
213
            if (func.functionType.kind === TokenKind.Function && func.returnTypeToken?.kind === TokenKind.Void) {
2!
214
                this.event.codeActions.push(
1✔
215
                    codeActionUtil.createCodeAction({
216
                        title: `Remove return type from function declaration`,
217
                        diagnostics: [diagnostic],
218
                        kind: CodeActionKind.QuickFix,
219
                        changes: [{
220
                            type: 'delete',
221
                            filePath: this.event.file.srcPath,
222
                            // )| as void|
223
                            range: util.createRange(
224
                                func.rightParen.range.start.line,
225
                                func.rightParen.range.start.character + 1,
226
                                func.returnTypeToken.range.end.line,
227
                                func.returnTypeToken.range.end.character
228
                            )
229
                        }]
230
                    })
231
                );
232
            }
233
        }
234
    }
235

236
    private addNonVoidFunctionReturnActions(diagnostic: Diagnostic) {
237
        if (isBrsFile(this.event.file)) {
2!
238
            const expression = this.event.file.getClosestExpression(diagnostic.range.start);
2✔
239
            const func = expression.findAncestor<FunctionExpression>(isFunctionExpression);
2✔
240

241
            //`sub as <non-void type>`, suggest removing the return type
242
            if (func.functionType.kind === TokenKind.Sub && func.returnTypeToken && func.returnTypeToken?.kind !== TokenKind.Void) {
2!
243
                this.event.codeActions.push(
1✔
244
                    codeActionUtil.createCodeAction({
245
                        title: `Remove return type from sub declaration`,
246
                        diagnostics: [diagnostic],
247
                        kind: CodeActionKind.QuickFix,
248
                        changes: [{
249
                            type: 'delete',
250
                            filePath: this.event.file.srcPath,
251
                            // )| as void|
252
                            range: util.createRange(
253
                                func.rightParen.range.start.line,
254
                                func.rightParen.range.start.character + 1,
255
                                func.returnTypeToken.range.end.line,
256
                                func.returnTypeToken.range.end.character
257
                            )
258
                        }]
259
                    })
260
                );
261
            }
262

263
            //function with no return type.
264
            if (func.functionType.kind === TokenKind.Function && !func.returnTypeToken) {
2✔
265
                //find tokens for `as` and `void` in the file if possible
266
                let asText: string;
267
                let voidText: string;
268
                let subText: string;
269
                let endSubText: string;
270
                for (const token of this.event.file.parser.tokens) {
1✔
271
                    if (asText && voidText && subText && endSubText) {
13!
272
                        break;
×
273
                    }
274
                    if (token?.kind === TokenKind.As) {
13!
275
                        asText = token?.text;
×
276
                    } else if (token?.kind === TokenKind.Void) {
13!
277
                        voidText = token?.text;
×
278
                    } else if (token?.kind === TokenKind.Sub) {
13!
279
                        subText = token?.text;
×
280
                    } else if (token?.kind === TokenKind.EndSub) {
13!
281
                        endSubText = token?.text;
×
282
                    }
283
                }
284

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