Skip to content

Commit 7b40fc7

Browse files
committed
revamp insert missing cases by leveraging actual AST transforms to produce the missing cases and insert them into the existing switch
1 parent ec14918 commit 7b40fc7

File tree

7 files changed

+112
-48
lines changed

7 files changed

+112
-48
lines changed

analysis/src/Cli.ml

+13
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,19 @@ let main () =
124124
Commands.codeAction ~path
125125
~pos:(int_of_string line, int_of_string col)
126126
~currentFile ~debug:false
127+
| [_; "codemod"; path; line; col; typ; hint] ->
128+
let typ =
129+
match typ with
130+
| "add-missing-cases" -> Codemod.AddMissingCases
131+
| _ -> raise (Failure "unsupported type")
132+
in
133+
let res =
134+
Codemod.transform ~path
135+
~pos:(int_of_string line, int_of_string col)
136+
~debug:false ~typ ~hint
137+
|> Json.escape
138+
in
139+
Printf.printf "\"%s\"" res
127140
| [_; "diagnosticSyntax"; path] -> Commands.diagnosticSyntax ~path
128141
| _ :: "reanalyze" :: _ ->
129142
let len = Array.length Sys.argv in

analysis/src/Codemod.ml

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
type transformType = AddMissingCases
2+
3+
let rec collectPatterns p =
4+
match p.Parsetree.ppat_desc with
5+
| Ppat_or (p1, p2) -> collectPatterns p1 @ [p2]
6+
| _ -> [p]
7+
8+
let mkFailWithExp () =
9+
Ast_helper.Exp.apply
10+
(Ast_helper.Exp.ident {txt = Lident "failwith"; loc = Location.none})
11+
[(Nolabel, Ast_helper.Exp.constant (Pconst_string ("TODO", None)))]
12+
13+
let transform ~path ~pos ~debug ~typ ~hint =
14+
let structure, printExpr, _ = Xform.parseImplementation ~filename:path in
15+
match typ with
16+
| AddMissingCases -> (
17+
let source = "let " ^ hint ^ " = ()" in
18+
let {Res_driver.parsetree = hintStructure} =
19+
Res_driver.parseImplementationFromSource ~forPrinter:false
20+
~displayFilename:"<none>" ~source
21+
in
22+
match hintStructure with
23+
| [{pstr_desc = Pstr_value (_, [{pvb_pat = pattern}])}] -> (
24+
let cases =
25+
collectPatterns pattern
26+
|> List.map (fun (p : Parsetree.pattern) ->
27+
Ast_helper.Exp.case p (mkFailWithExp ()))
28+
in
29+
let result = ref None in
30+
let mkIterator ~pos ~result =
31+
let expr (iterator : Ast_iterator.iterator) (exp : Parsetree.expression)
32+
=
33+
match exp.pexp_desc with
34+
| Pexp_match (e, existingCases)
35+
when Pos.ofLexing exp.pexp_loc.loc_start = pos ->
36+
result :=
37+
Some {exp with pexp_desc = Pexp_match (e, existingCases @ cases)}
38+
| _ -> Ast_iterator.default_iterator.expr iterator exp
39+
in
40+
{Ast_iterator.default_iterator with expr}
41+
in
42+
let iterator = mkIterator ~pos ~result in
43+
iterator.structure iterator structure;
44+
match !result with
45+
| None ->
46+
if debug then print_endline "Found no result";
47+
exit 1
48+
| Some switchExpr ->
49+
printExpr ~range:(Xform.rangeOfLoc switchExpr.pexp_loc) switchExpr)
50+
| _ ->
51+
if debug then print_endline "Mismatch in expected structure";
52+
exit 1)

analysis/src/Commands.ml

+8
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,14 @@ let test ~path =
392392
Printf.printf "%s\nnewText:\n%s<--here\n%s%s\n"
393393
(Protocol.stringifyRange range)
394394
indent indent newText)))
395+
| "c-a" ->
396+
let hint = String.sub rest 3 (String.length rest - 3) in
397+
print_endline
398+
("Codemod AddMissingCases" ^ path ^ " " ^ string_of_int line ^ ":"
399+
^ string_of_int col);
400+
Codemod.transform ~path ~pos:(line, col) ~debug:true
401+
~typ:AddMissingCases ~hint
402+
|> print_endline
395403
| "dia" -> diagnosticSyntax ~path
396404
| "hin" ->
397405
(* Get all inlay Hint between line 1 and n.

analysis/tests/bsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"subdirs": true
1010
}
1111
],
12-
"bsc-flags": ["-w -33-44"],
12+
"bsc-flags": ["-w -33-44-8"],
1313
"bs-dependencies": ["@rescript/react"],
1414
"jsx": { "version": 3 }
1515
}

analysis/tests/src/Codemod.res

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
type someTyp = [#valid | #invalid]
2+
3+
let ff = (v1: someTyp, v2: someTyp) => {
4+
let x = switch (v1, v2) {
5+
// ^c-a (#valid, #valid) | (#invalid, _)
6+
| (#valid, #invalid) => ()
7+
}
8+
x
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Codemod AddMissingCasessrc/Codemod.res 3:10
2+
switch (v1, v2) {
3+
// ^c-a (#valid, #valid) | (#invalid, _)
4+
| (#valid, #invalid) => ()
5+
| (#valid, #valid) => failwith("TODO")
6+
| (#invalid, _) => failwith("TODO")
7+
}
8+

server/src/codeActions.ts

+21-47
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// actions available in the extension, but they are derived via the analysis
33
// OCaml binary.
44
import * as p from "vscode-languageserver-protocol";
5+
import * as utils from "./utils";
6+
import { fileURLToPath } from "url";
57

68
export type filesCodeActions = {
79
[key: string]: { range: p.Range; codeAction: p.CodeAction }[];
@@ -82,6 +84,15 @@ let insertBeforeEndingChar = (
8284
];
8385
};
8486

87+
let replaceText = (range: p.Range, newText: string): p.TextEdit[] => {
88+
return [
89+
{
90+
range,
91+
newText,
92+
},
93+
];
94+
};
95+
8596
let removeTrailingComma = (text: string): string => {
8697
let str = text.trim();
8798
if (str.endsWith(",")) {
@@ -514,67 +525,30 @@ let simpleAddMissingCases: codeActionExtractor = ({
514525
if (
515526
line.startsWith("You forgot to handle a possible case here, for example:")
516527
) {
517-
let cases: string[] = [];
518-
519528
// This collects the rest of the fields if fields are printed on
520529
// multiple lines.
521530
let allCasesAsOneLine = array
522531
.slice(index + 1)
523532
.join("")
524533
.trim();
525534

526-
// We only handle the simplest possible cases until the compiler actually
527-
// outputs ReScript. This means bailing on anything that's not a
528-
// variant/polyvariant, with one payload (or no payloads at all).
529-
let openParensCount = allCasesAsOneLine.split("(").length - 1;
530-
531-
if (openParensCount > 1 || allCasesAsOneLine.includes("{")) {
532-
return false;
533-
}
534-
535-
// Remove surrounding braces if they exist
536-
if (allCasesAsOneLine[0] === "(") {
537-
allCasesAsOneLine = allCasesAsOneLine.slice(
538-
1,
539-
allCasesAsOneLine.length - 1
540-
);
541-
}
542-
543-
cases.push(
544-
...(allCasesAsOneLine
545-
.split("|")
546-
.map(transformMatchPattern)
547-
.filter(Boolean) as string[])
548-
);
549-
550-
if (cases.length === 0) {
551-
return false;
552-
}
553-
554-
// The end char is the closing brace. In switches, the leading `|` always
555-
// has the same left padding as the end brace.
556-
let paddingContentSwitchCase = Array.from({
557-
length: range.end.character,
558-
}).join(" ");
559-
560-
let newText = cases
561-
.map((variantName, index) => {
562-
// The first case will automatically be padded because we're inserting
563-
// it where the end brace is currently located.
564-
let padding = index === 0 ? "" : paddingContentSwitchCase;
565-
return `${padding}| ${variantName} => assert false`;
566-
})
567-
.join("\n");
535+
let filePath = fileURLToPath(file);
568536

569-
// Let's put the end brace back where it was (we still have it to the direct right of us).
570-
newText += `\n${paddingContentSwitchCase}`;
537+
let newSwitchCode = utils.runAnalysisAfterSanityCheck(filePath, [
538+
"codemod",
539+
filePath,
540+
range.start.line,
541+
range.start.character,
542+
"add-missing-cases",
543+
allCasesAsOneLine,
544+
]);
571545

572546
codeActions[file] = codeActions[file] || [];
573547
let codeAction: p.CodeAction = {
574548
title: `Insert missing cases`,
575549
edit: {
576550
changes: {
577-
[file]: insertBeforeEndingChar(range, newText),
551+
[file]: replaceText(range, newSwitchCode),
578552
},
579553
},
580554
diagnostics: [diagnostic],

0 commit comments

Comments
 (0)