diff --git a/jscomp/bsc/rescript_compiler_main.ml b/jscomp/bsc/rescript_compiler_main.ml index 16e927f42b..369abd3fcb 100644 --- a/jscomp/bsc/rescript_compiler_main.ml +++ b/jscomp/bsc/rescript_compiler_main.ml @@ -273,6 +273,9 @@ let buckle_script_flags : (string * Bsc_args.spec * string) array = "-bs-ast", unit_call(fun _ -> Js_config.binary_ast := true; Js_config.syntax_only := true), "*internal* Generate binary .mli_ast and ml_ast and stop"; + "-code-action-data", unit_call(fun _ -> Js_config.code_action_data := true), + "*internal* Emit code action data"; + "-bs-syntax-only", set Js_config.syntax_only, "*internal* Only check syntax"; diff --git a/jscomp/build_tests/editor_tooling_enhancements/.gitignore b/jscomp/build_tests/editor_tooling_enhancements/.gitignore new file mode 100644 index 0000000000..e0473fd9cf --- /dev/null +++ b/jscomp/build_tests/editor_tooling_enhancements/.gitignore @@ -0,0 +1 @@ +fixtures/*.js diff --git a/jscomp/build_tests/editor_tooling_enhancements/README.md b/jscomp/build_tests/editor_tooling_enhancements/README.md new file mode 100644 index 0000000000..12ae608779 --- /dev/null +++ b/jscomp/build_tests/editor_tooling_enhancements/README.md @@ -0,0 +1,5 @@ +Special tests for the editor tooling enhancements mode. + +Follow CONTRIBUTING.md and build the project, then run `node ./jscomp/build_tests/editor_tooling_enhancements/input.js` at the root of the project to check the tests against previous snapshots. + +Run `node ./jscomp/build_tests/editor_tooling_enhancements/input.js update` to update the snapshots. diff --git a/jscomp/build_tests/editor_tooling_enhancements/expected/replace_name.res.expected b/jscomp/build_tests/editor_tooling_enhancements/expected/replace_name.res.expected new file mode 100644 index 0000000000..780d265269 --- /dev/null +++ b/jscomp/build_tests/editor_tooling_enhancements/expected/replace_name.res.expected @@ -0,0 +1,16 @@ + + We've found a bug for you! + /.../fixtures/replace_name.res:8:3-6 + + 6 │ let x: x = { + 7 │ name: "hello", + 8 │ agee: 10, + 9 │ } + 10 │ + + The field agee does not belong to type x + + This record expression is expected to have type x +Hint: Did you mean age? +=== CODE ACTIONS === +[{"title": "Replace with `agee`", "kind": "quickfix" "loc": {"start": {"line": 8, "col": 74},"end": {"line": 8, "col": 78}}, "type": "replaceWith", "replaceWith": "agee"}] \ No newline at end of file diff --git a/jscomp/build_tests/editor_tooling_enhancements/expected/wrap_option_in_some.res.expected b/jscomp/build_tests/editor_tooling_enhancements/expected/wrap_option_in_some.res.expected new file mode 100644 index 0000000000..64a3618793 --- /dev/null +++ b/jscomp/build_tests/editor_tooling_enhancements/expected/wrap_option_in_some.res.expected @@ -0,0 +1,16 @@ + + We've found a bug for you! + /.../fixtures/wrap_option_in_some.res:2:3 + + 1 │ switch Some(1) { + 2 │ | 1 => () + 3 │ | _ => () + 4 │ } + + This pattern matches values of type int + but a pattern was expected which matches values of type option + + The value you're pattern matching on here is wrapped in an option, but you're trying to match on the actual value. + Wrap the highlighted pattern in Some() to make it work. +=== CODE ACTIONS === +[{"title": "Wrap in `Some()`", "kind": "quickfix" "loc": {"start": {"line": 2, "col": 19},"end": {"line": 2, "col": 20}}, "type": "wrapWith", "wrapLeft": "Some(", "wrapRight": ")"}] \ No newline at end of file diff --git a/jscomp/build_tests/editor_tooling_enhancements/fixtures/replace_name.res b/jscomp/build_tests/editor_tooling_enhancements/fixtures/replace_name.res new file mode 100644 index 0000000000..b154fbaf76 --- /dev/null +++ b/jscomp/build_tests/editor_tooling_enhancements/fixtures/replace_name.res @@ -0,0 +1,9 @@ +type x = { + name: string, + age: int, +} + +let x: x = { + name: "hello", + agee: 10, +} diff --git a/jscomp/build_tests/editor_tooling_enhancements/fixtures/wrap_option_in_some.res b/jscomp/build_tests/editor_tooling_enhancements/fixtures/wrap_option_in_some.res new file mode 100644 index 0000000000..1674431e5c --- /dev/null +++ b/jscomp/build_tests/editor_tooling_enhancements/fixtures/wrap_option_in_some.res @@ -0,0 +1,4 @@ +switch Some(1) { +| 1 => () +| _ => () +} diff --git a/jscomp/build_tests/editor_tooling_enhancements/input.js b/jscomp/build_tests/editor_tooling_enhancements/input.js new file mode 100644 index 0000000000..64d8890b56 --- /dev/null +++ b/jscomp/build_tests/editor_tooling_enhancements/input.js @@ -0,0 +1,64 @@ +const fs = require("fs"); +const path = require("path"); +const child_process = require("child_process"); + +const { bsc_exe: bsc } = require("#cli/bin_path"); + +const expectedDir = path.join(__dirname, "expected"); + +const fixtures = fs + .readdirSync(path.join(__dirname, "fixtures")) + .filter((fileName) => path.extname(fileName) === ".res"); + +// const runtime = path.join(__dirname, '..', '..', 'runtime') +const prefix = `${bsc} -w +A -code-action-data`; + +const updateTests = process.argv[2] === "update"; + +function postProcessErrorOutput(output) { + output = output.trimRight(); + output = output.replace( + /\/[^ ]+?jscomp\/build_tests\/editor_tooling_enhancements\//g, + "/.../" + ); + return output; +} + +let doneTasksCount = 0; +let atLeastOneTaskFailed = false; + +fixtures.forEach((fileName) => { + const fullFilePath = path.join(__dirname, "fixtures", fileName); + const command = `${prefix} -color always ${fullFilePath}`; + console.log(`running ${command}`); + child_process.exec(command, (err, stdout, stderr) => { + doneTasksCount++; + // careful of: + // - warning test that actually succeeded in compiling (warning's still in stderr, so the code path is shared here) + // - accidentally succeeding tests (not likely in this context), + // actual, correctly erroring test case + const actualErrorOutput = postProcessErrorOutput(stderr.toString()); + const expectedFilePath = path.join(expectedDir, fileName + ".expected"); + if (updateTests) { + fs.writeFileSync(expectedFilePath, actualErrorOutput); + } else { + const expectedErrorOutput = postProcessErrorOutput( + fs.readFileSync(expectedFilePath, { encoding: "utf-8" }) + ); + if (expectedErrorOutput !== actualErrorOutput) { + console.error( + `The old and new error output for the test ${fullFilePath} aren't the same` + ); + console.error("\n=== Old:"); + console.error(expectedErrorOutput); + console.error("\n=== New:"); + console.error(actualErrorOutput); + atLeastOneTaskFailed = true; + } + + if (doneTasksCount === fixtures.length && atLeastOneTaskFailed) { + process.exit(1); + } + } + }); +}); diff --git a/jscomp/common/js_config.ml b/jscomp/common/js_config.ml index 84f4e22f68..c5854cc3b1 100644 --- a/jscomp/common/js_config.ml +++ b/jscomp/common/js_config.ml @@ -57,6 +57,7 @@ let all_module_aliases = ref false let no_stdlib = ref false let no_export = ref false let as_ppx = ref false +let code_action_data = ref false let int_of_jsx_version = function | Jsx_v3 -> 3 diff --git a/jscomp/common/js_config.mli b/jscomp/common/js_config.mli index 31855eaca7..58253c291a 100644 --- a/jscomp/common/js_config.mli +++ b/jscomp/common/js_config.mli @@ -112,3 +112,5 @@ val as_pp : bool ref val self_stack : string Stack.t val modules : bool ref + +val code_action_data : bool ref diff --git a/jscomp/ml/code_action_data.ml b/jscomp/ml/code_action_data.ml new file mode 100644 index 0000000000..aa8cc91dc7 --- /dev/null +++ b/jscomp/ml/code_action_data.ml @@ -0,0 +1,89 @@ +type code_action_type = WrapWith of {left: string; right: string} | ReplaceWith of string +type code_action_style = Regular | QuickFix +type code_action = { + style: code_action_style; + type_: code_action_type; + title: string; +} + +let code_actions_enabled = ref true + +let code_action_data = ref [] +let add_code_action (data : code_action) = + code_action_data := data :: !code_action_data +let get_code_action_data () = !code_action_data + +let escape text = + let ln = String.length text in + let buf = Buffer.create ln in + let rec loop i = + if i < ln then ( + (match text.[i] with + | '\012' -> Buffer.add_string buf "\\f" + | '\\' -> Buffer.add_string buf "\\\\" + | '"' -> Buffer.add_string buf "\\\"" + | '\n' -> Buffer.add_string buf "\\n" + | '\b' -> Buffer.add_string buf "\\b" + | '\r' -> Buffer.add_string buf "\\r" + | '\t' -> Buffer.add_string buf "\\t" + | c -> Buffer.add_char buf c); + loop (i + 1)) + in + loop 0; + Buffer.contents buf + +let loc_to_json loc = + Printf.sprintf + "{\"start\": {\"line\": %s, \"col\": %s},\"end\": {\"line\": %s, \"col\": \ + %s}}" + (loc.Location.loc_start.pos_lnum |> string_of_int) + (loc.loc_start.pos_cnum |> string_of_int) + (loc.loc_end.pos_lnum |> string_of_int) + (loc.loc_end.pos_cnum |> string_of_int) + +let code_action_type_to_json = function + | WrapWith {left; right} -> + Printf.sprintf "\"type\": \"wrapWith\", \"wrapLeft\": \"%s\", \"wrapRight\": \"%s\"" + (escape left) (escape right) + | ReplaceWith text -> + Printf.sprintf "\"type\": \"replaceWith\", \"replaceWith\": \"%s\"" + (escape text) + +let emit_code_actions_data loc ppf = + match !code_action_data with + | [] -> () + | code_actions -> + Format.fprintf ppf "@\n=== CODE ACTIONS ===@\n["; + Format.fprintf ppf "%s" + (code_actions + |> List.map (fun data -> + Format.sprintf + "{\"title\": \"%s\", \"kind\": \"%s\" \"loc\": %s, %s}" + (escape data.title) + (match data.style with + | Regular -> "regular" + | QuickFix -> "quickfix") + (loc_to_json loc) + (code_action_type_to_json data.type_)) + |> String.concat ","); + Format.fprintf ppf "]" + + +module Actions = struct + let add_replace_with name = + if !code_actions_enabled then + add_code_action + { + style = QuickFix; + type_ = ReplaceWith name; + title = "Replace with `" ^ name ^ "`"; + } + let add_wrap_in_constructor name = + if !code_actions_enabled then + add_code_action + { + style = QuickFix; + type_ = WrapWith {left = name ^ "("; right = ")"}; + title = "Wrap in `" ^ name ^ "()`"; + } +end \ No newline at end of file diff --git a/jscomp/ml/code_action_data.mli b/jscomp/ml/code_action_data.mli new file mode 100644 index 0000000000..eca254d803 --- /dev/null +++ b/jscomp/ml/code_action_data.mli @@ -0,0 +1,17 @@ +type code_action_type = WrapWith of {left: string; right: string} | ReplaceWith of string +type code_action_style = Regular | QuickFix +type code_action = { + style: code_action_style; + type_: code_action_type; + title: string; +} + +val add_code_action: code_action -> unit +val get_code_action_data: unit -> code_action list + +val emit_code_actions_data: Location.t -> Format.formatter -> unit + +module Actions : sig + val add_replace_with: string -> unit + val add_wrap_in_constructor: string -> unit +end \ No newline at end of file diff --git a/jscomp/ml/error_message_utils.ml b/jscomp/ml/error_message_utils.ml index d178f2129f..e332820874 100644 --- a/jscomp/ml/error_message_utils.ml +++ b/jscomp/ml/error_message_utils.ml @@ -234,6 +234,7 @@ let print_contextual_unification_error ppf t1 t2 = | Tconstr (p1, _, _), Tconstr (p2, _, _) when Path.same p2 Predef.path_option && Path.same p1 Predef.path_option <> true -> + Code_action_data.Actions.add_wrap_in_constructor "Some"; fprintf ppf "@,@\n\ @[The value you're pattern matching on here is wrapped in an \ diff --git a/jscomp/ml/typecore.ml b/jscomp/ml/typecore.ml index 564a828db4..43a5871f60 100644 --- a/jscomp/ml/typecore.ml +++ b/jscomp/ml/typecore.ml @@ -636,6 +636,14 @@ let simple_conversions = [ let print_simple_conversion ppf (actual, expected) = try ( let converter = List.assoc (actual, expected) simple_conversions in + Code_action_data.add_code_action { + Code_action_data.style = QuickFix; + type_ = WrapWith { + left = converter ^ "("; + right = ")" + }; + title = Printf.sprintf "Convert %s to %s" actual expected + }; fprintf ppf "@,@,@[You can convert @{%s@} to @{%s@} with @{%s@}.@]" actual expected converter ) with | Not_found -> () @@ -3746,6 +3754,7 @@ let type_expression env sexp = (* Error report *) let spellcheck ppf unbound_name valid_names = + Code_action_data.Actions.add_replace_with unbound_name; Misc.did_you_mean ppf (fun () -> Misc.spellcheck valid_names unbound_name ) @@ -4045,14 +4054,14 @@ let report_error env ppf = function let super_report_error_no_wrap_printing_env = report_error -let report_error env ppf err = - Printtyp.wrap_printing_env env (fun () -> report_error env ppf err) +let report_error loc env ppf err = + Printtyp.wrap_printing_env env (fun () -> report_error env ppf err; Code_action_data.emit_code_actions_data loc ppf;) let () = Location.register_error_of_exn (function | Error (loc, env, err) -> - Some (Location.error_of_printer loc (report_error env) err) + Some (Location.error_of_printer loc (report_error loc env) err) | Error_forward err -> Some err | _ -> diff --git a/jscomp/ml/typecore.mli b/jscomp/ml/typecore.mli index 4b1fcc5368..3ba25b1b3e 100644 --- a/jscomp/ml/typecore.mli +++ b/jscomp/ml/typecore.mli @@ -114,7 +114,7 @@ exception Error_forward of Location.error val super_report_error_no_wrap_printing_env: Env.t -> formatter -> error -> unit -val report_error: Env.t -> formatter -> error -> unit +val report_error: Location.t -> Env.t -> formatter -> error -> unit (* Deprecated. Use Location.{error_of_exn, report_error}. *) (* Forward declaration, to be filled in by Typemod.type_module *)