Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PoC: emit code actions from the compiler that the editor tooling can use #7040

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions jscomp/bsc/rescript_compiler_main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Comment on lines +276 to +278
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't actually used yet, but we can probably have a bsc config like this.

"-bs-syntax-only", set Js_config.syntax_only,
"*internal* Only check syntax";

Expand Down
1 change: 1 addition & 0 deletions jscomp/build_tests/editor_tooling_enhancements/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fixtures/*.js
5 changes: 5 additions & 0 deletions jscomp/build_tests/editor_tooling_enhancements/README.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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"}]
Original file line number Diff line number Diff line change
@@ -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<int>

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": ")"}]
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
type x = {
name: string,
age: int,
}

let x: x = {
name: "hello",
agee: 10,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
switch Some(1) {
| 1 => ()
| _ => ()
}
64 changes: 64 additions & 0 deletions jscomp/build_tests/editor_tooling_enhancements/input.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
});
});
1 change: 1 addition & 0 deletions jscomp/common/js_config.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions jscomp/common/js_config.mli
Original file line number Diff line number Diff line change
Expand Up @@ -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
89 changes: 89 additions & 0 deletions jscomp/ml/code_action_data.ml
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions jscomp/ml/code_action_data.mli
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions jscomp/ml/error_message_utils.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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\
@[<v 0>The value you're pattern matching on here is wrapped in an \
Expand Down
15 changes: 12 additions & 3 deletions jscomp/ml/typecore.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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 "@,@,@[<v 2>You can convert @{<info>%s@} to @{<info>%s@} with @{<info>%s@}.@]" actual expected converter
) with | Not_found -> ()

Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -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
| _ ->
Expand Down
2 changes: 1 addition & 1 deletion jscomp/ml/typecore.mli
Original file line number Diff line number Diff line change
Expand Up @@ -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 *)
Expand Down
Loading