brighterscript 0.70.4 → 0.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/DiagnosticMessages.d.ts +15 -2
  2. package/dist/DiagnosticMessages.js +14 -1
  3. package/dist/DiagnosticMessages.js.map +1 -1
  4. package/dist/LanguageServer.js +6 -1
  5. package/dist/LanguageServer.js.map +1 -1
  6. package/dist/Program.js +1 -1
  7. package/dist/Program.js.map +1 -1
  8. package/dist/Scope.js +17 -6
  9. package/dist/Scope.js.map +1 -1
  10. package/dist/astUtils/reflection.d.ts +2 -1
  11. package/dist/astUtils/reflection.js +5 -1
  12. package/dist/astUtils/reflection.js.map +1 -1
  13. package/dist/astUtils/visitors.d.ts +2 -1
  14. package/dist/astUtils/visitors.js.map +1 -1
  15. package/dist/bscPlugin/codeActions/CodeActionsProcessor.d.ts +72 -6
  16. package/dist/bscPlugin/codeActions/CodeActionsProcessor.js +392 -110
  17. package/dist/bscPlugin/codeActions/CodeActionsProcessor.js.map +1 -1
  18. package/dist/bscPlugin/codeActions/CodeActionsProcessor.spec.js +575 -0
  19. package/dist/bscPlugin/codeActions/CodeActionsProcessor.spec.js.map +1 -1
  20. package/dist/bscPlugin/validation/ScopeValidator.d.ts +6 -0
  21. package/dist/bscPlugin/validation/ScopeValidator.js +70 -0
  22. package/dist/bscPlugin/validation/ScopeValidator.js.map +1 -1
  23. package/dist/parser/AstNode.spec.js +21 -0
  24. package/dist/parser/AstNode.spec.js.map +1 -1
  25. package/dist/parser/Expression.d.ts +25 -2
  26. package/dist/parser/Expression.js +41 -5
  27. package/dist/parser/Expression.js.map +1 -1
  28. package/dist/parser/Parser.js +25 -7
  29. package/dist/parser/Parser.js.map +1 -1
  30. package/dist/parser/tests/expression/AssociativeArrayLiterals.spec.js +70 -0
  31. package/dist/parser/tests/expression/AssociativeArrayLiterals.spec.js.map +1 -1
  32. package/dist/parser/tests/statement/Enum.spec.js +318 -0
  33. package/dist/parser/tests/statement/Enum.spec.js.map +1 -1
  34. package/package.json +1 -1
@@ -7,35 +7,129 @@ const DiagnosticMessages_1 = require("../../DiagnosticMessages");
7
7
  const Parser_1 = require("../../parser/Parser");
8
8
  const util_1 = require("../../util");
9
9
  const reflection_1 = require("../../astUtils/reflection");
10
+ const visitors_1 = require("../../astUtils/visitors");
10
11
  const TokenKind_1 = require("../../lexer/TokenKind");
11
12
  class CodeActionsProcessor {
12
13
  constructor(event) {
13
14
  this.event = event;
14
15
  this.suggestedImports = new Set();
15
16
  }
17
+ /**
18
+ * Processes all diagnostics in the event and emits code actions for each recognized diagnostic code.
19
+ */
16
20
  process() {
21
+ // First pass: individual fixes for each diagnostic at the cursor position
17
22
  for (const diagnostic of this.event.diagnostics) {
18
23
  if (diagnostic.code === DiagnosticMessages_1.DiagnosticCodeMap.cannotFindName || diagnostic.code === DiagnosticMessages_1.DiagnosticCodeMap.cannotFindFunction) {
19
- this.suggestCannotFindName(diagnostic);
24
+ this.suggestCannotFindNameQuickFix(diagnostic);
20
25
  }
21
26
  else if (diagnostic.code === DiagnosticMessages_1.DiagnosticCodeMap.classCouldNotBeFound) {
22
- this.suggestClassImports(diagnostic);
27
+ this.suggestClassImportQuickFix(diagnostic);
23
28
  }
24
29
  else if (diagnostic.code === DiagnosticMessages_1.DiagnosticCodeMap.xmlComponentMissingExtendsAttribute) {
25
- this.addMissingExtends(diagnostic);
30
+ this.suggestMissingExtendsQuickFix(diagnostic);
26
31
  }
27
32
  else if (diagnostic.code === DiagnosticMessages_1.DiagnosticCodeMap.voidFunctionMayNotReturnValue) {
28
- this.addVoidFunctionReturnActions(diagnostic);
33
+ this.suggestVoidFunctionReturnQuickFixes([diagnostic]);
29
34
  }
30
35
  else if (diagnostic.code === DiagnosticMessages_1.DiagnosticCodeMap.nonVoidFunctionMustReturnValue) {
31
- this.addNonVoidFunctionReturnActions(diagnostic);
36
+ this.suggestNonVoidFunctionReturnQuickFixes([diagnostic]);
37
+ }
38
+ else if (diagnostic.code === DiagnosticMessages_1.DiagnosticCodeMap.referencedFileDoesNotExist) {
39
+ this.suggestRemoveScriptImportQuickFixes([diagnostic]);
40
+ }
41
+ else if (diagnostic.code === DiagnosticMessages_1.DiagnosticCodeMap.unnecessaryScriptImportInChildFromParent) {
42
+ this.suggestRemoveScriptImportQuickFixes([diagnostic]);
43
+ }
44
+ else if (diagnostic.code === DiagnosticMessages_1.DiagnosticCodeMap.unnecessaryCodebehindScriptImport) {
45
+ this.suggestRemoveScriptImportQuickFixes([diagnostic]);
46
+ }
47
+ else if (diagnostic.code === DiagnosticMessages_1.DiagnosticCodeMap.scriptImportCaseMismatch) {
48
+ this.suggestScriptImportCasingQuickFixes([diagnostic]);
49
+ }
50
+ else if (diagnostic.code === DiagnosticMessages_1.DiagnosticCodeMap.missingOverrideKeyword) {
51
+ this.suggestMissingOverrideQuickFixes([diagnostic]);
52
+ }
53
+ else if (diagnostic.code === DiagnosticMessages_1.DiagnosticCodeMap.cannotUseOverrideKeywordOnConstructorFunction) {
54
+ this.suggestRemoveOverrideFromConstructorQuickFixes([diagnostic]);
55
+ }
56
+ }
57
+ // Second pass: fix-all actions for any code that appeared in the event.
58
+ // Also makes sure that fix-all actions appear after individual fixes
59
+ const eventCodes = new Set(this.event.diagnostics.map(d => d.code));
60
+ const fixAllDiagsByCode = this.collectFixAllDiagnostics(eventCodes);
61
+ // only offer fix-all when there are multiple instances of the same issue in the file
62
+ for (const [code, allInFile] of fixAllDiagsByCode) {
63
+ if (allInFile.length > 1) {
64
+ if (code === DiagnosticMessages_1.DiagnosticCodeMap.voidFunctionMayNotReturnValue) {
65
+ this.suggestVoidFunctionReturnQuickFixes(allInFile);
66
+ }
67
+ else if (code === DiagnosticMessages_1.DiagnosticCodeMap.nonVoidFunctionMustReturnValue) {
68
+ this.suggestNonVoidFunctionReturnQuickFixes(allInFile);
69
+ }
70
+ else if (code === DiagnosticMessages_1.DiagnosticCodeMap.unnecessaryCodebehindScriptImport) {
71
+ this.suggestRemoveScriptImportQuickFixes(allInFile);
72
+ }
73
+ else if (code === DiagnosticMessages_1.DiagnosticCodeMap.cannotUseOverrideKeywordOnConstructorFunction) {
74
+ this.suggestRemoveOverrideFromConstructorQuickFixes(allInFile);
75
+ }
76
+ else if (code === DiagnosticMessages_1.DiagnosticCodeMap.referencedFileDoesNotExist) {
77
+ this.suggestRemoveScriptImportQuickFixes(allInFile);
78
+ }
79
+ else if (code === DiagnosticMessages_1.DiagnosticCodeMap.unnecessaryScriptImportInChildFromParent) {
80
+ this.suggestRemoveScriptImportQuickFixes(allInFile);
81
+ }
82
+ else if (code === DiagnosticMessages_1.DiagnosticCodeMap.scriptImportCaseMismatch) {
83
+ this.suggestScriptImportCasingQuickFixes(allInFile);
84
+ }
85
+ else if (code === DiagnosticMessages_1.DiagnosticCodeMap.missingOverrideKeyword) {
86
+ this.suggestMissingOverrideQuickFixes(allInFile);
87
+ }
32
88
  }
33
89
  }
90
+ // Import fix-all aggregates across multiple codes so it runs as its own step
91
+ if (eventCodes.has(DiagnosticMessages_1.DiagnosticCodeMap.cannotFindName) ||
92
+ eventCodes.has(DiagnosticMessages_1.DiagnosticCodeMap.cannotFindFunction) ||
93
+ eventCodes.has(DiagnosticMessages_1.DiagnosticCodeMap.classCouldNotBeFound)) {
94
+ this.suggestMissingImportsFixAllQuickFix();
95
+ }
96
+ this.suggestedImports.clear();
97
+ }
98
+ /**
99
+ * Builds a map of diagnostic code → all matching diagnostics in the current file for each
100
+ * code in `eventCodes`. Scope-level codes are not present in `file.getDiagnostics()` so they
101
+ * are sourced from `program.getDiagnostics()` (fetched lazily, only when needed).
102
+ */
103
+ collectFixAllDiagnostics(eventCodes) {
104
+ var _a;
105
+ const scopeLevelCodes = new Set([
106
+ DiagnosticMessages_1.DiagnosticCodeMap.referencedFileDoesNotExist,
107
+ DiagnosticMessages_1.DiagnosticCodeMap.unnecessaryScriptImportInChildFromParent,
108
+ DiagnosticMessages_1.DiagnosticCodeMap.scriptImportCaseMismatch,
109
+ DiagnosticMessages_1.DiagnosticCodeMap.missingOverrideKeyword
110
+ ]);
111
+ const fileDiagsByCode = new Map();
112
+ for (const d of this.event.file.getDiagnostics()) {
113
+ if (!fileDiagsByCode.has(d.code)) {
114
+ fileDiagsByCode.set(d.code, []);
115
+ }
116
+ fileDiagsByCode.get(d.code).push(d);
117
+ }
118
+ const allScopeFileDiags = [...eventCodes].some(c => scopeLevelCodes.has(c))
119
+ ? this.event.program.getDiagnostics().filter(d => d.file === this.event.file)
120
+ : [];
121
+ const result = new Map();
122
+ for (const code of eventCodes) {
123
+ result.set(code, scopeLevelCodes.has(code)
124
+ ? allScopeFileDiags.filter(d => d.code === code)
125
+ : (_a = fileDiagsByCode.get(code)) !== null && _a !== void 0 ? _a : []);
126
+ }
127
+ return result;
34
128
  }
35
129
  /**
36
130
  * Generic import suggestion function. Shouldn't be called directly from the main loop, but instead called by more specific diagnostic handlers
37
131
  */
38
- suggestImports(diagnostic, key, files) {
132
+ suggestImportQuickFix(diagnostic, key, files) {
39
133
  var _a, _b, _c;
40
134
  //skip if we already have this suggestion
41
135
  if (this.suggestedImports.has(key)) {
@@ -62,29 +156,102 @@ class CodeActionsProcessor {
62
156
  }));
63
157
  }
64
158
  }
65
- suggestCannotFindName(diagnostic) {
159
+ /**
160
+ * Suggests import statements for an unresolved name (function, class, namespace, or enum).
161
+ */
162
+ suggestCannotFindNameQuickFix(diagnostic) {
66
163
  var _a;
67
164
  //skip if not a BrighterScript file
68
165
  if (diagnostic.file.parseMode !== Parser_1.ParseMode.BrighterScript) {
69
166
  return;
70
167
  }
71
168
  const lowerName = ((_a = diagnostic.data.fullName) !== null && _a !== void 0 ? _a : diagnostic.data.name).toLowerCase();
72
- this.suggestImports(diagnostic, lowerName, [
169
+ this.suggestImportQuickFix(diagnostic, lowerName, [
73
170
  ...this.event.file.program.findFilesForFunction(lowerName),
74
171
  ...this.event.file.program.findFilesForClass(lowerName),
75
172
  ...this.event.file.program.findFilesForNamespace(lowerName),
76
173
  ...this.event.file.program.findFilesForEnum(lowerName)
77
174
  ]);
78
175
  }
79
- suggestClassImports(diagnostic) {
176
+ /**
177
+ * Suggests import statements for an unresolved class name.
178
+ */
179
+ suggestClassImportQuickFix(diagnostic) {
80
180
  //skip if not a BrighterScript file
81
181
  if (diagnostic.file.parseMode !== Parser_1.ParseMode.BrighterScript) {
82
182
  return;
83
183
  }
84
184
  const lowerClassName = diagnostic.data.className.toLowerCase();
85
- this.suggestImports(diagnostic, lowerClassName, this.event.file.program.findFilesForClass(lowerClassName));
185
+ this.suggestImportQuickFix(diagnostic, lowerClassName, this.event.file.program.findFilesForClass(lowerClassName));
86
186
  }
87
- addMissingExtends(diagnostic) {
187
+ /**
188
+ * Scans all import-related diagnostics in the file and emits a single composite
189
+ * "Fix all: Add missing imports" action when 2+ unambiguous imports are needed.
190
+ * Ambiguous names (multiple possible source files) are excluded since we cannot
191
+ * automatically choose one.
192
+ */
193
+ suggestMissingImportsFixAllQuickFix() {
194
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
195
+ if (!(0, reflection_1.isBrsFile)(this.event.file) || this.event.file.parseMode !== Parser_1.ParseMode.BrighterScript) {
196
+ return;
197
+ }
198
+ const file = this.event.file;
199
+ const importStatements = file.parser.references.importStatements;
200
+ const insertPosition = (_c = (_b = (_a = importStatements[importStatements.length - 1]) === null || _a === void 0 ? void 0 : _a.importToken.range) === null || _b === void 0 ? void 0 : _b.start) !== null && _c !== void 0 ? _c : util_1.util.createPosition(0, 0);
201
+ const changes = [];
202
+ const addedPaths = new Set();
203
+ // cannotFindName/classCouldNotBeFound are scope-level diagnostics, so we must
204
+ // use program.getDiagnostics() (filtered by file) rather than file.getDiagnostics().
205
+ const allFileDiagnostics = this.event.program.getDiagnostics().filter(d => d.file === file);
206
+ for (const diagnostic of allFileDiagnostics) {
207
+ let files = [];
208
+ if (diagnostic.code === DiagnosticMessages_1.DiagnosticCodeMap.cannotFindName || diagnostic.code === DiagnosticMessages_1.DiagnosticCodeMap.cannotFindFunction) {
209
+ const cannotFindNameDiagnostic = diagnostic;
210
+ const lowerName = (_g = ((_e = (_d = cannotFindNameDiagnostic.data) === null || _d === void 0 ? void 0 : _d.fullName) !== null && _e !== void 0 ? _e : (_f = cannotFindNameDiagnostic.data) === null || _f === void 0 ? void 0 : _f.name)) === null || _g === void 0 ? void 0 : _g.toLowerCase();
211
+ if (lowerName) {
212
+ files = [
213
+ ...file.program.findFilesForFunction(lowerName),
214
+ ...file.program.findFilesForClass(lowerName),
215
+ ...file.program.findFilesForNamespace(lowerName),
216
+ ...file.program.findFilesForEnum(lowerName)
217
+ ];
218
+ }
219
+ }
220
+ else if (diagnostic.code === DiagnosticMessages_1.DiagnosticCodeMap.classCouldNotBeFound) {
221
+ const classCouldNotBeFoundDiagnostic = diagnostic;
222
+ const lowerClassName = (_j = (_h = classCouldNotBeFoundDiagnostic.data) === null || _h === void 0 ? void 0 : _h.className) === null || _j === void 0 ? void 0 : _j.toLowerCase();
223
+ if (lowerClassName) {
224
+ files = file.program.findFilesForClass(lowerClassName);
225
+ }
226
+ }
227
+ //skip ambiguous names — we can't choose a file automatically
228
+ if (files.length !== 1) {
229
+ continue;
230
+ }
231
+ const pkgPath = util_1.util.getRokuPkgPath(files[0].pkgPath);
232
+ if (!addedPaths.has(pkgPath)) {
233
+ addedPaths.add(pkgPath);
234
+ changes.push({
235
+ type: 'insert',
236
+ filePath: file.srcPath,
237
+ position: insertPosition,
238
+ newText: `import "${pkgPath}"\n`
239
+ });
240
+ }
241
+ }
242
+ if (changes.length > 1) {
243
+ this.event.codeActions.push(CodeActionUtil_1.codeActionUtil.createCodeAction({
244
+ title: `Fix all: Auto fixable missing imports`,
245
+ kind: vscode_languageserver_1.CodeActionKind.QuickFix,
246
+ changes: changes
247
+ }));
248
+ }
249
+ }
250
+ /**
251
+ * Adds code actions to insert a missing `extends` attribute on an XML component tag.
252
+ * Offers Group, Task, and ContentNode as common choices.
253
+ */
254
+ suggestMissingExtendsQuickFix(diagnostic) {
88
255
  var _a;
89
256
  const srcPath = this.event.file.srcPath;
90
257
  const { component } = this.event.file.parser.ast;
@@ -125,19 +292,17 @@ class CodeActionsProcessor {
125
292
  }]
126
293
  }));
127
294
  }
128
- addVoidFunctionReturnActions(diagnostic) {
295
+ /**
296
+ * Adds code actions to resolve a `voidFunctionMayNotReturnValue` diagnostic.
297
+ * Offers removing the return value, converting sub→function, or removing an `as void` return type.
298
+ */
299
+ suggestVoidFunctionReturnQuickFixes(diagnostics) {
129
300
  var _a, _b, _c, _d;
130
- this.event.codeActions.push(CodeActionUtil_1.codeActionUtil.createCodeAction({
131
- title: `Remove return value`,
132
- diagnostics: [diagnostic],
133
- kind: vscode_languageserver_1.CodeActionKind.QuickFix,
134
- changes: [{
135
- type: 'delete',
136
- filePath: this.event.file.srcPath,
137
- range: util_1.util.createRange(diagnostic.range.start.line, diagnostic.range.start.character + 'return'.length, diagnostic.range.end.line, diagnostic.range.end.character)
138
- }]
139
- }));
140
- if ((0, reflection_1.isBrsFile)(this.event.file)) {
301
+ const changes = diagnostics.map(d => this.getRemoveReturnValueChange(d));
302
+ this.emitOrFixAll(`Remove return value`, `Fix all: Remove void return values`, changes, diagnostics[0]);
303
+ //contextual BrsFile actions only apply to the individual (single-violation) case
304
+ if (changes.length === 1 && (0, reflection_1.isBrsFile)(this.event.file)) {
305
+ const diagnostic = diagnostics[0];
141
306
  const expression = this.event.file.getClosestExpression(diagnostic.range.start);
142
307
  const func = expression.findAncestor(reflection_1.isFunctionExpression);
143
308
  //if we're in a sub and we do not have a return type, suggest converting to a function
@@ -154,19 +319,9 @@ class CodeActionsProcessor {
154
319
  kind: vscode_languageserver_1.CodeActionKind.QuickFix,
155
320
  changes: [
156
321
  //function
157
- {
158
- type: 'replace',
159
- filePath: this.event.file.srcPath,
160
- range: func.functionType.range,
161
- newText: functionTypeText
162
- },
322
+ { type: 'replace', filePath: this.event.file.srcPath, range: func.functionType.range, newText: functionTypeText },
163
323
  //end function
164
- {
165
- type: 'replace',
166
- filePath: this.event.file.srcPath,
167
- range: func.end.range,
168
- newText: endFunctionTypeText
169
- }
324
+ { type: 'replace', filePath: this.event.file.srcPath, range: func.end.range, newText: endFunctionTypeText }
170
325
  ]
171
326
  }));
172
327
  }
@@ -176,90 +331,217 @@ class CodeActionsProcessor {
176
331
  title: `Remove return type from function declaration`,
177
332
  diagnostics: [diagnostic],
178
333
  kind: vscode_languageserver_1.CodeActionKind.QuickFix,
179
- changes: [{
180
- type: 'delete',
181
- filePath: this.event.file.srcPath,
182
- // )| as void|
183
- range: util_1.util.createRange(func.rightParen.range.start.line, func.rightParen.range.start.character + 1, func.returnTypeToken.range.end.line, func.returnTypeToken.range.end.character)
184
- }]
334
+ changes: [this.getRemoveFunctionReturnTypeChange(func)]
185
335
  }));
186
336
  }
187
337
  }
188
338
  }
189
- addNonVoidFunctionReturnActions(diagnostic) {
339
+ /**
340
+ * Adds code actions to resolve a `nonVoidFunctionMustReturnValue` diagnostic.
341
+ * Offers removing the return type from a sub, adding `as void` to a function, or converting function→sub.
342
+ */
343
+ suggestNonVoidFunctionReturnQuickFixes(diagnostics) {
344
+ if (!(0, reflection_1.isBrsFile)(this.event.file)) {
345
+ return;
346
+ }
347
+ const file = this.event.file;
348
+ //find tokens for `as`, `void`, `sub`, `end sub` in the file if possible
349
+ let asText;
350
+ let voidText;
351
+ let subText;
352
+ let endSubText;
353
+ for (const token of file.parser.tokens) {
354
+ if (asText && voidText && subText && endSubText) {
355
+ break;
356
+ }
357
+ if ((token === null || token === void 0 ? void 0 : token.kind) === TokenKind_1.TokenKind.As) {
358
+ asText = token === null || token === void 0 ? void 0 : token.text;
359
+ }
360
+ else if ((token === null || token === void 0 ? void 0 : token.kind) === TokenKind_1.TokenKind.Void) {
361
+ voidText = token === null || token === void 0 ? void 0 : token.text;
362
+ }
363
+ else if ((token === null || token === void 0 ? void 0 : token.kind) === TokenKind_1.TokenKind.Sub) {
364
+ subText = token === null || token === void 0 ? void 0 : token.text;
365
+ }
366
+ else if ((token === null || token === void 0 ? void 0 : token.kind) === TokenKind_1.TokenKind.EndSub) {
367
+ endSubText = token === null || token === void 0 ? void 0 : token.text;
368
+ }
369
+ }
370
+ // Build per-fix-type change arrays, deduplicating by enclosing function so that one
371
+ // function with multiple bare returns only contributes one change.
372
+ const removeReturnTypeChanges = [];
373
+ const addVoidChanges = [];
374
+ const seenFunctions = new Set();
375
+ for (const d of diagnostics) {
376
+ const expr = file.getClosestExpression(d.range.start);
377
+ const fn = expr === null || expr === void 0 ? void 0 : expr.findAncestor(reflection_1.isFunctionExpression);
378
+ if (!fn) {
379
+ continue;
380
+ }
381
+ const fnKey = `${fn.range.start.line}:${fn.range.start.character}`;
382
+ if (seenFunctions.has(fnKey)) {
383
+ continue;
384
+ }
385
+ seenFunctions.add(fnKey);
386
+ if (fn.functionType.kind === TokenKind_1.TokenKind.Sub && fn.returnTypeToken && fn.returnTypeToken.kind !== TokenKind_1.TokenKind.Void) {
387
+ removeReturnTypeChanges.push(this.getRemoveFunctionReturnTypeChange(fn));
388
+ }
389
+ else if (fn.functionType.kind === TokenKind_1.TokenKind.Function && !fn.returnTypeToken) {
390
+ addVoidChanges.push({
391
+ type: 'insert',
392
+ filePath: this.event.file.srcPath,
393
+ position: fn.rightParen.range.end,
394
+ newText: ` ${asText !== null && asText !== void 0 ? asText : 'as'} ${voidText !== null && voidText !== void 0 ? voidText : 'void'}`
395
+ });
396
+ }
397
+ }
398
+ this.emitOrFixAll(`Remove return type from sub declaration`, `Fix all: Remove return type from sub declarations`, removeReturnTypeChanges, diagnostics[0]);
399
+ this.emitOrFixAll(`Add void return type to function declaration`, `Fix all: Add void return type to function declarations`, addVoidChanges, diagnostics[0]);
400
+ //'Convert function to sub' has no fix-all variant; only add it for the individual case
401
+ if (addVoidChanges.length === 1 && diagnostics.length === 1) {
402
+ const func = file.getClosestExpression(diagnostics[0].range.start).findAncestor(reflection_1.isFunctionExpression);
403
+ this.event.codeActions.push(CodeActionUtil_1.codeActionUtil.createCodeAction({
404
+ title: `Convert function to sub`,
405
+ diagnostics: [diagnostics[0]],
406
+ kind: vscode_languageserver_1.CodeActionKind.QuickFix,
407
+ changes: [
408
+ { type: 'replace', filePath: file.srcPath, range: func.functionType.range, newText: subText !== null && subText !== void 0 ? subText : 'sub' },
409
+ { type: 'replace', filePath: file.srcPath, range: func.end.range, newText: endSubText !== null && endSubText !== void 0 ? endSubText : 'end sub' }
410
+ ]
411
+ }));
412
+ }
413
+ }
414
+ // ---- script import fixes ----
415
+ /**
416
+ * Adds code actions to delete one or more unnecessary or broken script import lines.
417
+ */
418
+ suggestRemoveScriptImportQuickFixes(diagnostics) {
419
+ var _a, _b;
420
+ const titles = {
421
+ [DiagnosticMessages_1.DiagnosticCodeMap.unnecessaryScriptImportInChildFromParent]: ['Remove redundant script import', 'Fix all: Remove redundant script imports'],
422
+ [DiagnosticMessages_1.DiagnosticCodeMap.unnecessaryCodebehindScriptImport]: ['Remove unnecessary codebehind import', 'Fix all: Remove unnecessary codebehind imports']
423
+ };
424
+ const [singleTitle, fixAllTitle] = (_b = titles[(_a = diagnostics[0]) === null || _a === void 0 ? void 0 : _a.code]) !== null && _b !== void 0 ? _b : ['Remove script import', 'Fix all: Remove script imports'];
425
+ const changes = diagnostics.map(diagnostic => {
426
+ return {
427
+ type: 'delete',
428
+ filePath: this.event.file.srcPath,
429
+ range: util_1.util.createRange(diagnostic.range.start.line, 0, diagnostic.range.start.line + 1, 0)
430
+ };
431
+ });
432
+ this.emitOrFixAll(singleTitle, fixAllTitle, changes, diagnostics[0]);
433
+ }
434
+ /**
435
+ * Adds code actions to correct the casing of script import paths to match the actual file name on disk.
436
+ */
437
+ suggestScriptImportCasingQuickFixes(diagnostics) {
190
438
  var _a;
191
- if ((0, reflection_1.isBrsFile)(this.event.file)) {
192
- const expression = this.event.file.getClosestExpression(diagnostic.range.start);
193
- const func = expression.findAncestor(reflection_1.isFunctionExpression);
194
- //`sub as <non-void type>`, suggest removing the return type
195
- if (func.functionType.kind === TokenKind_1.TokenKind.Sub && func.returnTypeToken && ((_a = func.returnTypeToken) === null || _a === void 0 ? void 0 : _a.kind) !== TokenKind_1.TokenKind.Void) {
196
- this.event.codeActions.push(CodeActionUtil_1.codeActionUtil.createCodeAction({
197
- title: `Remove return type from sub declaration`,
198
- diagnostics: [diagnostic],
199
- kind: vscode_languageserver_1.CodeActionKind.QuickFix,
200
- changes: [{
201
- type: 'delete',
202
- filePath: this.event.file.srcPath,
203
- // )| as void|
204
- range: util_1.util.createRange(func.rightParen.range.start.line, func.rightParen.range.start.character + 1, func.returnTypeToken.range.end.line, func.returnTypeToken.range.end.character)
205
- }]
206
- }));
439
+ const changes = [];
440
+ for (const diagnostic of diagnostics) {
441
+ const correctFilePath = (_a = diagnostic.data) === null || _a === void 0 ? void 0 : _a.correctFilePath;
442
+ if (!correctFilePath) {
443
+ continue;
207
444
  }
208
- //function with no return type.
209
- if (func.functionType.kind === TokenKind_1.TokenKind.Function && !func.returnTypeToken) {
210
- //find tokens for `as` and `void` in the file if possible
211
- let asText;
212
- let voidText;
213
- let subText;
214
- let endSubText;
215
- for (const token of this.event.file.parser.tokens) {
216
- if (asText && voidText && subText && endSubText) {
217
- break;
218
- }
219
- if ((token === null || token === void 0 ? void 0 : token.kind) === TokenKind_1.TokenKind.As) {
220
- asText = token === null || token === void 0 ? void 0 : token.text;
221
- }
222
- else if ((token === null || token === void 0 ? void 0 : token.kind) === TokenKind_1.TokenKind.Void) {
223
- voidText = token === null || token === void 0 ? void 0 : token.text;
224
- }
225
- else if ((token === null || token === void 0 ? void 0 : token.kind) === TokenKind_1.TokenKind.Sub) {
226
- subText = token === null || token === void 0 ? void 0 : token.text;
227
- }
228
- else if ((token === null || token === void 0 ? void 0 : token.kind) === TokenKind_1.TokenKind.EndSub) {
229
- endSubText = token === null || token === void 0 ? void 0 : token.text;
230
- }
445
+ changes.push({
446
+ type: 'replace',
447
+ filePath: this.event.file.srcPath,
448
+ range: diagnostic.range,
449
+ newText: correctFilePath
450
+ });
451
+ }
452
+ this.emitOrFixAll('Fix script import path casing', 'Fix all: Fix script import path casing', changes, diagnostics[0]);
453
+ }
454
+ // ---- override keyword fixes ----
455
+ /**
456
+ * Adds code actions to insert the missing `override` keyword before a method declaration.
457
+ */
458
+ suggestMissingOverrideQuickFixes(diagnostics) {
459
+ if (!(0, reflection_1.isBrsFile)(this.event.file)) {
460
+ return;
461
+ }
462
+ const file = this.event.file;
463
+ const changes = [];
464
+ for (const diagnostic of diagnostics) {
465
+ let insertPosition;
466
+ file.ast.walk((node) => {
467
+ var _a, _b, _c, _d, _e, _f;
468
+ if ((0, reflection_1.isMethodStatement)(node) &&
469
+ ((_b = (_a = node.range) === null || _a === void 0 ? void 0 : _a.start) === null || _b === void 0 ? void 0 : _b.line) === diagnostic.range.start.line &&
470
+ ((_d = (_c = node.range) === null || _c === void 0 ? void 0 : _c.start) === null || _d === void 0 ? void 0 : _d.character) === diagnostic.range.start.character) {
471
+ insertPosition = (_f = (_e = node.func.functionType) === null || _e === void 0 ? void 0 : _e.range) === null || _f === void 0 ? void 0 : _f.start;
231
472
  }
232
- //suggest converting to `as void`
233
- this.event.codeActions.push(CodeActionUtil_1.codeActionUtil.createCodeAction({
234
- title: `Add void return type to function declaration`,
235
- diagnostics: [diagnostic],
236
- kind: vscode_languageserver_1.CodeActionKind.QuickFix,
237
- changes: [{
238
- type: 'insert',
239
- filePath: this.event.file.srcPath,
240
- position: func.rightParen.range.end,
241
- newText: ` ${asText !== null && asText !== void 0 ? asText : 'as'} ${voidText !== null && voidText !== void 0 ? voidText : 'void'}`
242
- }]
243
- }));
244
- //suggest converting to sub
245
- this.event.codeActions.push(CodeActionUtil_1.codeActionUtil.createCodeAction({
246
- title: `Convert function to sub`,
247
- diagnostics: [diagnostic],
248
- kind: vscode_languageserver_1.CodeActionKind.QuickFix,
249
- changes: [{
250
- type: 'replace',
251
- filePath: this.event.file.srcPath,
252
- range: func.functionType.range,
253
- newText: subText !== null && subText !== void 0 ? subText : 'sub'
254
- }, {
255
- type: 'replace',
256
- filePath: this.event.file.srcPath,
257
- range: func.end.range,
258
- newText: endSubText !== null && endSubText !== void 0 ? endSubText : 'end sub'
259
- }]
260
- }));
473
+ }, { walkMode: visitors_1.WalkMode.visitStatementsRecursive });
474
+ if (insertPosition) {
475
+ changes.push({
476
+ type: 'insert',
477
+ filePath: file.srcPath,
478
+ position: insertPosition,
479
+ newText: 'override '
480
+ });
261
481
  }
262
482
  }
483
+ this.emitOrFixAll(`Add missing 'override' keyword`, `Fix all: Add missing 'override' keywords`, changes, diagnostics[0]);
484
+ }
485
+ /**
486
+ * Adds code actions to remove the invalid `override` keyword from a constructor method.
487
+ */
488
+ suggestRemoveOverrideFromConstructorQuickFixes(diagnostics) {
489
+ const changes = diagnostics.map(d => ({
490
+ type: 'delete',
491
+ filePath: this.event.file.srcPath,
492
+ // delete "override " — the keyword token plus the trailing space before function/sub
493
+ range: util_1.util.createRange(d.range.start.line, d.range.start.character, d.range.end.line, d.range.end.character + 1)
494
+ }));
495
+ this.emitOrFixAll(`Remove 'override' from constructor`, `Fix all: Remove 'override' from constructors`, changes, diagnostics[0]);
496
+ }
497
+ // ---- change helpers ----
498
+ /**
499
+ * Builds a delete change that removes the return value from a `return <expr>` statement,
500
+ * leaving just a bare `return`.
501
+ */
502
+ getRemoveReturnValueChange(diagnostic) {
503
+ return {
504
+ type: 'delete',
505
+ filePath: this.event.file.srcPath,
506
+ range: util_1.util.createRange(diagnostic.range.start.line, diagnostic.range.start.character + 'return'.length, diagnostic.range.end.line, diagnostic.range.end.character)
507
+ };
508
+ }
509
+ /**
510
+ * Builds the change that deletes `) as <type>` from a function/sub declaration.
511
+ * Used for both `as void` on a function and any return type on a sub.
512
+ */
513
+ getRemoveFunctionReturnTypeChange(func) {
514
+ return {
515
+ type: 'delete',
516
+ filePath: this.event.file.srcPath,
517
+ // )| as <type>|
518
+ range: util_1.util.createRange(func.rightParen.range.start.line, func.rightParen.range.start.character + 1, func.returnTypeToken.range.end.line, func.returnTypeToken.range.end.character)
519
+ };
520
+ }
521
+ /**
522
+ * Emits a single code action when there is exactly one change, or a "fix all" composite
523
+ * action when there are multiple changes (same pattern as ESLint's "Fix all X problems").
524
+ * Does nothing when the changes array is empty.
525
+ */
526
+ emitOrFixAll(singleTitle, fixAllTitle, changes, diagnostic) {
527
+ if (changes.length === 0) {
528
+ return;
529
+ }
530
+ if (changes.length === 1) {
531
+ this.event.codeActions.push(CodeActionUtil_1.codeActionUtil.createCodeAction({
532
+ title: singleTitle,
533
+ diagnostics: [diagnostic],
534
+ kind: vscode_languageserver_1.CodeActionKind.QuickFix,
535
+ changes: changes
536
+ }));
537
+ }
538
+ else {
539
+ this.event.codeActions.push(CodeActionUtil_1.codeActionUtil.createCodeAction({
540
+ title: fixAllTitle,
541
+ kind: vscode_languageserver_1.CodeActionKind.QuickFix,
542
+ changes: changes
543
+ }));
544
+ }
263
545
  }
264
546
  }
265
547
  exports.CodeActionsProcessor = CodeActionsProcessor;