Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
5f6f3ca
base setup for actions + tests
zth Jul 26, 2025
3a1b585
implement the actual rewriting
zth Jul 26, 2025
0a477f4
map
zth Jul 26, 2025
817d0d6
add and remove await
zth Jul 26, 2025
e4cca31
rewrite object to record
zth Jul 26, 2025
c1012ec
rewrite array to tuple
zth Jul 26, 2025
c2beafb
more array to tuple
zth Jul 26, 2025
0d3ac29
jsx conversions
zth Jul 26, 2025
a49633b
comments + rewrite ident
zth Jul 26, 2025
25928c9
more todo comments for actions that could be useful
zth Jul 27, 2025
76cf124
format
zth Jul 27, 2025
85f8f5c
more todo comments
zth Jul 27, 2025
d4cdbe3
refactor to centralize generating actions from warnings
zth Jul 27, 2025
3b1179f
move remaining warning driven actions to centralized place
zth Jul 27, 2025
74a875d
add value_bindings to Ast_mapper
zth Jul 27, 2025
d55b9b5
prefix unused
zth Jul 27, 2025
0c3cdcb
add value_bindings to Ast_iterator as well
zth Jul 27, 2025
cdfcd55
format
zth Jul 27, 2025
793193b
spellcheck
zth Jul 28, 2025
edbf177
allow filtering actions, and add test for removing unused var entirely
zth Jul 28, 2025
dbc2673
emit all available actions in a comment in applied file
zth Jul 28, 2025
32c5b71
fix ident-to-module action
zth Jul 28, 2025
916cc24
unused value declarations
zth Jul 28, 2025
2fa3bd5
remove unused modules and types
zth Jul 28, 2025
21b045d
remove unused rec flag
zth Jul 28, 2025
a088a23
format
zth Jul 28, 2025
8f6e470
force open
zth Jul 28, 2025
d403fbe
cleanup
zth Jul 28, 2025
308b155
remove record spread
zth Jul 28, 2025
4edd281
remove irrelevant
zth Jul 28, 2025
4696a4a
handle top level
zth Jul 28, 2025
4cdd4e7
clenaup
zth Jul 28, 2025
f95f138
emit all available actions into applied file, not just the filtered ones
zth Jul 28, 2025
a48322d
make optional arg labelled
zth Jul 28, 2025
c7a9b7d
labelled to optional arg
zth Jul 28, 2025
f6a0c3c
partially apply function
zth Jul 28, 2025
724caca
add missing args
zth Jul 28, 2025
37d0d90
pass record field expr as optional
zth Jul 28, 2025
3f31581
add action for automatically unwrapping record field access through o…
zth Jul 29, 2025
34dba54
add test files
zth Jul 29, 2025
ecdeeba
fix syntax error
zth Sep 19, 2025
54726fc
move away from cmt to an explicit sidecar extras file
zth Sep 20, 2025
1052070
test output
zth Sep 20, 2025
6c4f866
fixes after merge
zth Oct 7, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ _build
*.cmx
*.cmt
*.cmti
*.resextra
*.cma
*.a
*.cmxa
Expand Down
3 changes: 2 additions & 1 deletion analysis/src/Cmt.ml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ let fullFromUri ~uri =
let cmt = getCmtPath ~uri paths in
fullForCmt ~moduleName ~package ~uri cmt
| None ->
prerr_endline ("can't find module " ^ moduleName);
if not (Uri.isInterface uri) then
prerr_endline ("can't find module " ^ moduleName);
None))

let fullsFromModule ~package ~moduleName =
Expand Down
30 changes: 30 additions & 0 deletions analysis/src/Resextra.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
let extrasPathFromCmtPath cmtPath =
if Filename.check_suffix cmtPath ".cmti" then
Filename.chop_extension cmtPath ^ ".resiextra"
else if Filename.check_suffix cmtPath ".cmt" then
Filename.chop_extension cmtPath ^ ".resextra"
else cmtPath ^ ".resextra"

let loadActionsFromPackage ~path ~package =
let uri = Uri.fromPath path in
let moduleName =
BuildSystem.namespacedName package.SharedTypes.namespace
(FindFiles.getName path)
in
match Hashtbl.find_opt package.SharedTypes.pathsForModule moduleName with
| None -> None
| Some paths ->
let cmtPath = SharedTypes.getCmtPath ~uri paths in
let extrasPath = extrasPathFromCmtPath cmtPath in

let tryLoad path =
if Sys.file_exists path then
try
let ic = open_in_bin path in
let v = (input_value ic : Actions.action list) in
close_in ic;
Some v
with _ -> None
else None
in
tryLoad extrasPath
28 changes: 27 additions & 1 deletion analysis/src/Xform.ml
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,28 @@ let parseInterface ~filename =
let extractCodeActions ~path ~startPos ~endPos ~currentFile ~debug =
let pos = startPos in
let codeActions = ref [] in
let add_actions_from_extras ~path ~pos ~package ~codeActions =
let map_extra_action (a : Actions.action) =
match a.action with
| Actions.RemoveOpen ->
let range = Loc.rangeOfLoc a.loc in
let newText = "" in
Some
(CodeActions.make ~title:a.description ~kind:RefactorRewrite ~uri:path
~newText ~range)
| _ -> None
in
match Resextra.loadActionsFromPackage ~path ~package with
| None -> ()
| Some actions ->
let relevant =
actions
|> List.filter (fun (a : Actions.action) -> Loc.hasPos ~pos a.loc)
in
relevant
|> List.filter_map map_extra_action
|> List.iter (fun ca -> codeActions := ca :: !codeActions)
in
match Files.classifySourceFile currentFile with
| Res ->
let structure, printExpr, printStructureItem, printStandaloneStructure =
Expand All @@ -920,7 +942,8 @@ let extractCodeActions ~path ~startPos ~endPos ~currentFile ~debug =
~pos:
(if startPos = endPos then Single startPos
else Range (startPos, endPos))
~full ~structure ~codeActions ~debug ~currentFile
~full ~structure ~codeActions ~debug ~currentFile;
add_actions_from_extras ~path ~pos ~package:full.package ~codeActions
| None -> ()
in

Expand All @@ -929,5 +952,8 @@ let extractCodeActions ~path ~startPos ~endPos ~currentFile ~debug =
let signature, printSignatureItem = parseInterface ~filename:currentFile in
AddDocTemplate.Interface.xform ~pos ~codeActions ~path ~signature
~printSignatureItem;
(match Packages.getPackage ~uri:(Uri.fromPath path) with
| Some package -> add_actions_from_extras ~path ~pos ~package ~codeActions
| None -> ());
!codeActions
| Other -> []
3 changes: 3 additions & 0 deletions compiler/bsc/rescript_compiler_main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,9 @@ let _ : unit =
Bs_conditional_initial.setup_env ();
Clflags.color := Some Always;

(* Save extras (e.g., actions) once before exit, after all reporting. *)
at_exit (fun () -> Res_extra.save ());

let flags = "flags" in
Ast_config.add_structure flags file_level_flags_handler;
Ast_config.add_signature flags file_level_flags_handler;
Expand Down
12 changes: 10 additions & 2 deletions compiler/core/js_implementation.ml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ let after_parsing_sig ppf outputprefix ast =
if !Js_config.syntax_only then Warnings.check_fatal ()
else
let modulename = module_of_filename outputprefix in
Res_extra.set_is_interface true;
Res_extra.set_current_outputprefix (Some outputprefix);
Lam_compile_env.reset ();
let initial_env = Res_compmisc.initial_env ~modulename () in
Env.set_unit_name modulename;
Expand All @@ -65,7 +67,9 @@ let after_parsing_sig ppf outputprefix ast =
in
Typemod.save_signature modulename tsg outputprefix !Location.input_name
initial_env sg;
process_with_gentype (outputprefix ^ ".cmti"))
process_with_gentype (outputprefix ^ ".cmti");
(* Persist any collected code actions to .resextra sidecar *)
Res_extra.save ())

let interface ~parser ppf ?outputprefix fname =
let outputprefix =
Expand Down Expand Up @@ -130,6 +134,8 @@ let after_parsing_impl ppf outputprefix (ast : Parsetree.structure) =
if !Js_config.syntax_only then Warnings.check_fatal ()
else
let modulename = Ext_filename.module_name outputprefix in
Res_extra.set_is_interface false;
Res_extra.set_current_outputprefix (Some outputprefix);
Lam_compile_env.reset ();
let env = Res_compmisc.initial_env ~modulename () in
Env.set_unit_name modulename;
Expand All @@ -152,7 +158,9 @@ let after_parsing_impl ppf outputprefix (ast : Parsetree.structure) =
in
if not !Js_config.cmj_only then
Lam_compile_main.lambda_as_module js_program outputprefix);
process_with_gentype (outputprefix ^ ".cmt"))
process_with_gentype (outputprefix ^ ".cmt");
(* Persist any collected code actions to .resextra sidecar *)
Res_extra.save ())

let implementation ~parser ppf ?outputprefix fname =
let outputprefix =
Expand Down
3 changes: 3 additions & 0 deletions compiler/ext/warnings.ml
Original file line number Diff line number Diff line change
Expand Up @@ -690,3 +690,6 @@ let loc_to_string (loc : loc) : string =
(loc.loc_start.pos_cnum - loc.loc_start.pos_bol)
loc.loc_end.pos_lnum
(loc.loc_end.pos_cnum - loc.loc_end.pos_bol)

let emit_possible_actions_from_warning : (loc -> t -> unit) ref =
ref (fun _ _ -> ())
2 changes: 2 additions & 0 deletions compiler/ext/warnings.mli
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,5 @@ val loc_to_string : loc -> string
(**
Turn the location into a string with (line,column--line,column) format.
*)

val emit_possible_actions_from_warning : (loc -> t -> unit) ref
182 changes: 182 additions & 0 deletions compiler/ml/actions.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
type deprecated_used_context = FunctionCall | Reference

type deprecated_used = {
source_loc: Location.t;
deprecated_text: string;
migration_template: Parsetree.expression option;
migration_in_pipe_chain_template: Parsetree.expression option;
context: deprecated_used_context option;
}

type cmt_extra_info = {deprecated_used: deprecated_used list}

let record_deprecated_used :
(?deprecated_context:deprecated_used_context ->
?migration_template:Parsetree.expression ->
?migration_in_pipe_chain_template:Parsetree.expression ->
Location.t ->
string ->
unit)
ref =
ref
(fun
?deprecated_context
?migration_template
?migration_in_pipe_chain_template
_
_
->
ignore deprecated_context;
ignore migration_template;
ignore migration_in_pipe_chain_template)
type action_type =
| ApplyFunction of {function_name: Longident.t}
| ApplyCoercion of {coerce_to_name: Longident.t}
| RemoveSwitchCase
| RemoveOpen
| RemoveAwait
| AddAwait
| ReplaceWithVariantConstructor of {constructor_name: Longident.t}
| ReplaceWithPolymorphicVariantConstructor of {constructor_name: string}
| RewriteObjectToRecord
| RewriteArrayToTuple
| RewriteIdentToModule of {module_name: string}
| RewriteIdent of {new_ident: Longident.t}
| RewriteArgType of {to_type: [`Labelled | `Optional | `Unlabelled]}
| PrefixVariableWithUnderscore
| RemoveUnusedVariable
| RemoveUnusedType
| RemoveUnusedModule
| RemoveRecFlag
| RemoveRecordSpread
| ForceOpen
| AssignToUnderscore
| PipeToIgnore
| PartiallyApplyFunction
| InsertMissingArguments of {missing_args: Asttypes.Noloc.arg_label list}
| ChangeRecordFieldOptional of {optional: bool}
| UnwrapOptionMapRecordField of {field_name: Longident.t}

(* TODO:
- Unused var in patterns (and aliases )*)

type action = {loc: Location.t; action: action_type; description: string}

let action_to_string = function
| ApplyFunction {function_name} ->
Printf.sprintf "ApplyFunction(%s)"
(Longident.flatten function_name |> String.concat ".")
| ApplyCoercion {coerce_to_name} ->
Printf.sprintf "ApplyCoercion(%s)"
(Longident.flatten coerce_to_name |> String.concat ".")
| RemoveSwitchCase -> "RemoveSwitchCase"
| RemoveOpen -> "RemoveOpen"
| RemoveAwait -> "RemoveAwait"
| AddAwait -> "AddAwait"
| RewriteObjectToRecord -> "RewriteObjectToRecord"
| RewriteArrayToTuple -> "RewriteArrayToTuple"
| RewriteIdentToModule {module_name} ->
Printf.sprintf "RewriteIdentToModule(%s)" module_name
| PrefixVariableWithUnderscore -> "PrefixVariableWithUnderscore"
| RemoveUnusedVariable -> "RemoveUnusedVariable"
| RemoveUnusedType -> "RemoveUnusedType"
| RemoveUnusedModule -> "RemoveUnusedModule"
| ReplaceWithVariantConstructor {constructor_name} ->
Printf.sprintf "ReplaceWithVariantConstructor(%s)"
(constructor_name |> Longident.flatten |> String.concat ".")
| ReplaceWithPolymorphicVariantConstructor {constructor_name} ->
Printf.sprintf "ReplaceWithPolymorphicVariantConstructor(%s)"
constructor_name
| RewriteIdent {new_ident} ->
Printf.sprintf "RewriteIdent(%s)"
(Longident.flatten new_ident |> String.concat ".")
| RemoveRecFlag -> "RemoveRecFlag"
| ForceOpen -> "ForceOpen"
| RemoveRecordSpread -> "RemoveRecordSpread"
| AssignToUnderscore -> "AssignToUnderscore"
| PipeToIgnore -> "PipeToIgnore"
| RewriteArgType {to_type} -> (
match to_type with
| `Labelled -> "RewriteArgType(Labelled)"
| `Optional -> "RewriteArgType(Optional)"
| `Unlabelled -> "RewriteArgType(Unlabelled)")
| PartiallyApplyFunction -> "PartiallyApplyFunction"
| InsertMissingArguments {missing_args} ->
Printf.sprintf "InsertMissingArguments(%s)"
(missing_args
|> List.map (fun arg ->
match arg with
| Asttypes.Noloc.Labelled txt -> "~" ^ txt
| Asttypes.Noloc.Optional txt -> "?" ^ txt
| Asttypes.Noloc.Nolabel -> "<unlabelled>")
|> String.concat ", ")
| ChangeRecordFieldOptional {optional} ->
Printf.sprintf "ChangeRecordFieldOptional(%s)"
(if optional then "true" else "false")
| UnwrapOptionMapRecordField {field_name} ->
Printf.sprintf "UnwrapOptionMapRecordField(%s)"
(Longident.flatten field_name |> String.concat ".")

let _add_possible_action : (action -> unit) ref = ref (fun _ -> ())
let add_possible_action action = !_add_possible_action action

let emit_possible_actions_from_warning loc w =
match w with
| Warnings.Unused_open _ ->
add_possible_action {loc; action = RemoveOpen; description = "Remove open"}
| Unused_match | Unreachable_case ->
add_possible_action
{loc; action = RemoveSwitchCase; description = "Remove switch case"}
| Unused_var _ | Unused_var_strict _ | Unused_value_declaration _ ->
add_possible_action
{
loc;
action = PrefixVariableWithUnderscore;
description = "Prefix with `_`";
};
add_possible_action
{
loc;
action = RemoveUnusedVariable;
description = "Remove unused variable";
}
| Unused_type_declaration _ ->
add_possible_action
{loc; action = RemoveUnusedType; description = "Remove unused type"}
| Unused_module _ ->
add_possible_action
{loc; action = RemoveUnusedModule; description = "Remove unused module"}
| Unused_rec_flag ->
add_possible_action
{loc; action = RemoveRecFlag; description = "Remove rec flag"}
| Open_shadow_identifier _ | Open_shadow_label_constructor _ ->
add_possible_action {loc; action = ForceOpen; description = "Force open"}
| Useless_record_with ->
add_possible_action
{loc; action = RemoveRecordSpread; description = "Remove `...` spread"}
| Bs_toplevel_expression_unit _ ->
add_possible_action
{loc; action = PipeToIgnore; description = "Pipe to ignore()"};
add_possible_action
{loc; action = AssignToUnderscore; description = "Assign to let _ ="}
| Nonoptional_label _ ->
add_possible_action
{
loc;
action = RewriteArgType {to_type = `Labelled};
description = "Make argument optional";
}
(*

=== TODO ===

*)
| Unused_pat -> (* Remove pattern *) ()
| Unused_argument -> (* Remove unused argument or prefix with underscore *) ()
| Unused_constructor _ -> (* Remove unused constructor *) ()
| Bs_unused_attribute _ -> (* Remove unused attribute *) ()
| _ -> ()

let _ =
Warnings.emit_possible_actions_from_warning :=
emit_possible_actions_from_warning
Loading
Loading