From cb88ad901e5dc7c4c98c7478825f198a6fca8033 Mon Sep 17 00:00:00 2001 From: nojaf Date: Fri, 19 Sep 2025 10:28:25 +0200 Subject: [PATCH 1/8] Add completion for throw keyword --- analysis/src/CompletionBackEnd.ml | 93 +++++++++++++++++++ tests/analysis_tests/tests/src/Throw.res | 5 + .../tests/src/expected/Completion.res.txt | 10 ++ .../src/expected/CompletionJsxProps.res.txt | 10 ++ .../tests/src/expected/Throw.res.txt | 74 +++++++++++++++ 5 files changed, 192 insertions(+) create mode 100644 tests/analysis_tests/tests/src/Throw.res create mode 100644 tests/analysis_tests/tests/src/expected/Throw.res.txt diff --git a/analysis/src/CompletionBackEnd.ml b/analysis/src/CompletionBackEnd.ml index bbd61eac48..469864ad17 100644 --- a/analysis/src/CompletionBackEnd.ml +++ b/analysis/src/CompletionBackEnd.ml @@ -756,6 +756,72 @@ let getCompletionsForPath ~debug ~opens ~full ~pos ~exact ~scope findAllCompletions ~env ~prefix ~exact ~namesUsed ~completionContext | None -> [])) +(* Collect exception constructor names from cmt infos. *) +let exceptions_from_cmt_infos (infos : Cmt_format.cmt_infos) : + (string * bool) list = + let by_name : (string, bool) Hashtbl.t = Hashtbl.create 16 in + let add_ext (ext : Typedtree.extension_constructor) : unit = + let name = ext.ext_name.txt in + let hasArgs = + match ext.ext_kind with + | Text_decl (Cstr_tuple args, _ret) -> args <> [] + | Text_decl (Cstr_record fields, _ret) -> fields <> [] + | Text_rebind _ -> true + in + let prev = + match Hashtbl.find_opt by_name name with + | Some b -> b + | None -> false + in + Hashtbl.replace by_name name (prev || hasArgs) + in + let rec of_structure_items (items : Typedtree.structure_item list) : unit = + match items with + | [] -> () + | item :: rest -> + (match item.str_desc with + | Tstr_exception ext -> add_ext ext + | _ -> ()); + of_structure_items rest + in + let rec of_signature_items (items : Typedtree.signature_item list) : unit = + match items with + | [] -> () + | item :: rest -> + (match item.sig_desc with + | Tsig_exception ext -> add_ext ext + | _ -> ()); + of_signature_items rest + in + let of_parts (parts : Cmt_format.binary_part array) : unit = + Array.iter + (function + | Cmt_format.Partial_structure s -> of_structure_items s.str_items + | Partial_structure_item si -> of_structure_items [si] + | Partial_signature s -> of_signature_items s.sig_items + | Partial_signature_item si -> of_signature_items [si] + | _ -> ()) + parts + in + (match infos.cmt_annots with + | Cmt_format.Implementation s -> of_structure_items s.str_items + | Interface s -> of_signature_items s.sig_items + | Partial_implementation parts -> of_parts parts + | Partial_interface parts -> of_parts parts + | _ -> ()); + Hashtbl.fold (fun name hasArgs acc -> (name, hasArgs) :: acc) by_name [] + +(* Predefined Stdlib/Pervasives exceptions. *) +let predefined_exceptions : (string * bool) list = + [ + ("Not_found", true); + ("Invalid_argument", true); + ("Assert_failure", true); + ("Failure", true); + ("Match_failure", true); + ("Division_by_zero", false); + ] + (** Completions intended for piping, from a completion path. *) let completionsForPipeFromCompletionPath ~envCompletionIsMadeFrom ~opens ~pos ~scope ~debug ~prefix ~env ~rawOpens ~full completionPath = @@ -1010,6 +1076,33 @@ and getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env ~exact | Some (Tpromise (env, typ), _env) -> [Completion.create "dummy" ~env ~kind:(Completion.Value typ)] | _ -> []) + | CPId {path = ["throw"]; completionContext = Value; loc = _} -> + let exn_typ = Ctype.newconstr Predef.path_exn [] in + let names_from_cmt = + let moduleName = env.file.moduleName in + match Hashtbl.find_opt full.package.pathsForModule moduleName with + | None -> [] + | Some paths -> ( + let uri = getUri paths in + let cmt_path = getCmtPath ~uri paths in + match Shared.tryReadCmt cmt_path with + | None -> [] + | Some infos -> exceptions_from_cmt_infos infos) + in + let all = names_from_cmt @ predefined_exceptions in + all + |> List.map (fun (name, hasArgs) -> + let insertText = + if hasArgs then Printf.sprintf "(%s($0))" name + else Printf.sprintf "(%s)" name + in + let isBuiltin = List.mem (name, hasArgs) predefined_exceptions in + let detail = + if isBuiltin then "Built-in Exception" + else "User-defined Exception" + in + Completion.create name ~env ~kind:(Completion.Value exn_typ) + ~includesSnippets:true ~insertText ~detail) | CPId {path; completionContext; loc} -> if Debug.verbose () then print_endline "[ctx_path]--> CPId"; (* Looks up the type of an identifier. diff --git a/tests/analysis_tests/tests/src/Throw.res b/tests/analysis_tests/tests/src/Throw.res new file mode 100644 index 0000000000..5066f26383 --- /dev/null +++ b/tests/analysis_tests/tests/src/Throw.res @@ -0,0 +1,5 @@ +exception MyCustomThingToThrow(string) +exception NoArgsToThrow + +// let x = () => throw +// ^com \ No newline at end of file diff --git a/tests/analysis_tests/tests/src/expected/Completion.res.txt b/tests/analysis_tests/tests/src/expected/Completion.res.txt index 0e525703b2..3182b8c445 100644 --- a/tests/analysis_tests/tests/src/expected/Completion.res.txt +++ b/tests/analysis_tests/tests/src/expected/Completion.res.txt @@ -2147,6 +2147,16 @@ Path T "modulePath": "TableclothMap", "filePath": "src/Completion.res" } + }, { + "label": "Throw", + "kind": 9, + "tags": [], + "detail": "module Throw", + "documentation": null, + "data": { + "modulePath": "Throw", + "filePath": "src/Completion.res" + } }, { "label": "TypeArgCtx", "kind": 9, diff --git a/tests/analysis_tests/tests/src/expected/CompletionJsxProps.res.txt b/tests/analysis_tests/tests/src/expected/CompletionJsxProps.res.txt index 686f4f5ec9..bcd1aca5f6 100644 --- a/tests/analysis_tests/tests/src/expected/CompletionJsxProps.res.txt +++ b/tests/analysis_tests/tests/src/expected/CompletionJsxProps.res.txt @@ -97,6 +97,16 @@ Path CompletionSupport.TestComponent.make "modulePath": "TableclothMap", "filePath": "src/CompletionJsxProps.res" } + }, { + "label": "Throw", + "kind": 9, + "tags": [], + "detail": "module Throw", + "documentation": null, + "data": { + "modulePath": "Throw", + "filePath": "src/CompletionJsxProps.res" + } }, { "label": "TypeArgCtx", "kind": 9, diff --git a/tests/analysis_tests/tests/src/expected/Throw.res.txt b/tests/analysis_tests/tests/src/expected/Throw.res.txt new file mode 100644 index 0000000000..bce6eec1ca --- /dev/null +++ b/tests/analysis_tests/tests/src/expected/Throw.res.txt @@ -0,0 +1,74 @@ +Complete src/Throw.res 3:22 +posCursor:[3:22] posNoWhite:[3:21] Found expr:[3:11->3:22] +posCursor:[3:22] posNoWhite:[3:21] Found expr:[3:17->3:22] +Pexp_ident throw:[3:17->3:22] +Completable: Cpath Value[throw] +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[throw] +[{ + "label": "MyCustomThingToThrow", + "kind": 12, + "tags": [], + "detail": "User-defined Exception", + "documentation": null, + "insertText": "(MyCustomThingToThrow($0))", + "insertTextFormat": 2 + }, { + "label": "NoArgsToThrow", + "kind": 12, + "tags": [], + "detail": "User-defined Exception", + "documentation": null, + "insertText": "(NoArgsToThrow)", + "insertTextFormat": 2 + }, { + "label": "Not_found", + "kind": 12, + "tags": [], + "detail": "Built-in Exception", + "documentation": null, + "insertText": "(Not_found($0))", + "insertTextFormat": 2 + }, { + "label": "Invalid_argument", + "kind": 12, + "tags": [], + "detail": "Built-in Exception", + "documentation": null, + "insertText": "(Invalid_argument($0))", + "insertTextFormat": 2 + }, { + "label": "Assert_failure", + "kind": 12, + "tags": [], + "detail": "Built-in Exception", + "documentation": null, + "insertText": "(Assert_failure($0))", + "insertTextFormat": 2 + }, { + "label": "Failure", + "kind": 12, + "tags": [], + "detail": "Built-in Exception", + "documentation": null, + "insertText": "(Failure($0))", + "insertTextFormat": 2 + }, { + "label": "Match_failure", + "kind": 12, + "tags": [], + "detail": "Built-in Exception", + "documentation": null, + "insertText": "(Match_failure($0))", + "insertTextFormat": 2 + }, { + "label": "Division_by_zero", + "kind": 12, + "tags": [], + "detail": "Built-in Exception", + "documentation": null, + "insertText": "(Division_by_zero)", + "insertTextFormat": 2 + }] + From 4d7ce2e4c7867620a44abbb5a486cb3be731602a Mon Sep 17 00:00:00 2001 From: nojaf Date: Fri, 19 Sep 2025 10:59:33 +0200 Subject: [PATCH 2/8] Try completions if path didn't have any results --- analysis/src/CompletionBackEnd.ml | 61 ++++++++++--------- tests/analysis_tests/tests/src/Throw.res | 4 +- .../tests/src/expected/Throw.res.txt | 53 +++++++++------- 3 files changed, 65 insertions(+), 53 deletions(-) diff --git a/analysis/src/CompletionBackEnd.ml b/analysis/src/CompletionBackEnd.ml index 469864ad17..fc20479361 100644 --- a/analysis/src/CompletionBackEnd.ml +++ b/analysis/src/CompletionBackEnd.ml @@ -1076,34 +1076,7 @@ and getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env ~exact | Some (Tpromise (env, typ), _env) -> [Completion.create "dummy" ~env ~kind:(Completion.Value typ)] | _ -> []) - | CPId {path = ["throw"]; completionContext = Value; loc = _} -> - let exn_typ = Ctype.newconstr Predef.path_exn [] in - let names_from_cmt = - let moduleName = env.file.moduleName in - match Hashtbl.find_opt full.package.pathsForModule moduleName with - | None -> [] - | Some paths -> ( - let uri = getUri paths in - let cmt_path = getCmtPath ~uri paths in - match Shared.tryReadCmt cmt_path with - | None -> [] - | Some infos -> exceptions_from_cmt_infos infos) - in - let all = names_from_cmt @ predefined_exceptions in - all - |> List.map (fun (name, hasArgs) -> - let insertText = - if hasArgs then Printf.sprintf "(%s($0))" name - else Printf.sprintf "(%s)" name - in - let isBuiltin = List.mem (name, hasArgs) predefined_exceptions in - let detail = - if isBuiltin then "Built-in Exception" - else "User-defined Exception" - in - Completion.create name ~env ~kind:(Completion.Value exn_typ) - ~includesSnippets:true ~insertText ~detail) - | CPId {path; completionContext; loc} -> + | CPId {path; completionContext; loc} -> ( if Debug.verbose () then print_endline "[ctx_path]--> CPId"; (* Looks up the type of an identifier. @@ -1141,7 +1114,37 @@ and getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env ~exact | _ -> byPath else byPath in - result + match (result, path) with + | [], [prefix] when Utils.startsWith "throw" prefix -> + let exn_typ = Ctype.newconstr Predef.path_exn [] in + let names_from_cmt = + let moduleName = env.file.moduleName in + match Hashtbl.find_opt full.package.pathsForModule moduleName with + | None -> [] + | Some paths -> ( + let uri = getUri paths in + let cmt_path = getCmtPath ~uri paths in + match Shared.tryReadCmt cmt_path with + | None -> [] + | Some infos -> exceptions_from_cmt_infos infos) + in + let all = names_from_cmt @ predefined_exceptions in + all + |> List.map (fun (name, hasArgs) -> + let insertText = + if hasArgs then Printf.sprintf "throw(%s($0))" name + else Printf.sprintf "throw(%s)" name + in + let isBuiltin = List.mem (name, hasArgs) predefined_exceptions in + let detail = + if isBuiltin then "Built-in Exception" + else "User-defined Exception" + in + Completion.create + (Printf.sprintf "throw(%s)" name) + ~env ~kind:(Completion.Value exn_typ) ~includesSnippets:true + ~insertText ~filterText:"throw" ~detail) + | _ -> result) | CPApply (cp, labels) -> ( if Debug.verbose () then print_endline "[ctx_path]--> CPApply"; match diff --git a/tests/analysis_tests/tests/src/Throw.res b/tests/analysis_tests/tests/src/Throw.res index 5066f26383..3876f82e5c 100644 --- a/tests/analysis_tests/tests/src/Throw.res +++ b/tests/analysis_tests/tests/src/Throw.res @@ -1,5 +1,5 @@ exception MyCustomThingToThrow(string) exception NoArgsToThrow -// let x = () => throw -// ^com \ No newline at end of file +// let x = () => thro +// ^com \ No newline at end of file diff --git a/tests/analysis_tests/tests/src/expected/Throw.res.txt b/tests/analysis_tests/tests/src/expected/Throw.res.txt index bce6eec1ca..7971c5a665 100644 --- a/tests/analysis_tests/tests/src/expected/Throw.res.txt +++ b/tests/analysis_tests/tests/src/expected/Throw.res.txt @@ -1,74 +1,83 @@ -Complete src/Throw.res 3:22 -posCursor:[3:22] posNoWhite:[3:21] Found expr:[3:11->3:22] -posCursor:[3:22] posNoWhite:[3:21] Found expr:[3:17->3:22] -Pexp_ident throw:[3:17->3:22] -Completable: Cpath Value[throw] +Complete src/Throw.res 3:21 +posCursor:[3:21] posNoWhite:[3:20] Found expr:[3:11->3:21] +posCursor:[3:21] posNoWhite:[3:20] Found expr:[3:17->3:21] +Pexp_ident thro:[3:17->3:21] +Completable: Cpath Value[thro] Package opens Stdlib.place holder Pervasives.JsxModules.place holder Resolved opens 1 Stdlib -ContextPath Value[throw] +ContextPath Value[thro] +Path thro [{ - "label": "MyCustomThingToThrow", + "label": "throw(MyCustomThingToThrow)", "kind": 12, "tags": [], "detail": "User-defined Exception", "documentation": null, - "insertText": "(MyCustomThingToThrow($0))", + "filterText": "throw", + "insertText": "throw(MyCustomThingToThrow($0))", "insertTextFormat": 2 }, { - "label": "NoArgsToThrow", + "label": "throw(NoArgsToThrow)", "kind": 12, "tags": [], "detail": "User-defined Exception", "documentation": null, - "insertText": "(NoArgsToThrow)", + "filterText": "throw", + "insertText": "throw(NoArgsToThrow)", "insertTextFormat": 2 }, { - "label": "Not_found", + "label": "throw(Not_found)", "kind": 12, "tags": [], "detail": "Built-in Exception", "documentation": null, - "insertText": "(Not_found($0))", + "filterText": "throw", + "insertText": "throw(Not_found($0))", "insertTextFormat": 2 }, { - "label": "Invalid_argument", + "label": "throw(Invalid_argument)", "kind": 12, "tags": [], "detail": "Built-in Exception", "documentation": null, - "insertText": "(Invalid_argument($0))", + "filterText": "throw", + "insertText": "throw(Invalid_argument($0))", "insertTextFormat": 2 }, { - "label": "Assert_failure", + "label": "throw(Assert_failure)", "kind": 12, "tags": [], "detail": "Built-in Exception", "documentation": null, - "insertText": "(Assert_failure($0))", + "filterText": "throw", + "insertText": "throw(Assert_failure($0))", "insertTextFormat": 2 }, { - "label": "Failure", + "label": "throw(Failure)", "kind": 12, "tags": [], "detail": "Built-in Exception", "documentation": null, - "insertText": "(Failure($0))", + "filterText": "throw", + "insertText": "throw(Failure($0))", "insertTextFormat": 2 }, { - "label": "Match_failure", + "label": "throw(Match_failure)", "kind": 12, "tags": [], "detail": "Built-in Exception", "documentation": null, - "insertText": "(Match_failure($0))", + "filterText": "throw", + "insertText": "throw(Match_failure($0))", "insertTextFormat": 2 }, { - "label": "Division_by_zero", + "label": "throw(Division_by_zero)", "kind": 12, "tags": [], "detail": "Built-in Exception", "documentation": null, - "insertText": "(Division_by_zero)", + "filterText": "throw", + "insertText": "throw(Division_by_zero)", "insertTextFormat": 2 }] From 497b37226ef98af084f22a9e87b845be6c078a1a Mon Sep 17 00:00:00 2001 From: nojaf Date: Fri, 19 Sep 2025 11:00:55 +0200 Subject: [PATCH 3/8] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b020f7241..a8b02656d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ #### :nail_care: Polish - Add (dev-)dependencies to build schema. https://github.com/rescript-lang/rescript/pull/7892 +- Add completions for `throw`. https://github.com/rescript-lang/rescript/pull/7905 #### :house: Internal From 678220d15e82153031d8bdf1dd2693cf82cb4e4d Mon Sep 17 00:00:00 2001 From: nojaf Date: Fri, 19 Sep 2025 11:04:20 +0200 Subject: [PATCH 4/8] extract to helper function --- analysis/src/CompletionBackEnd.ml | 58 ++++++++++++++++--------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/analysis/src/CompletionBackEnd.ml b/analysis/src/CompletionBackEnd.ml index fc20479361..61f7f0bf29 100644 --- a/analysis/src/CompletionBackEnd.ml +++ b/analysis/src/CompletionBackEnd.ml @@ -822,6 +822,35 @@ let predefined_exceptions : (string * bool) list = ("Division_by_zero", false); ] +let completionsForThrow ~(env : QueryEnv.t) ~full = + let exn_typ = Ctype.newconstr Predef.path_exn [] in + let names_from_cmt = + let moduleName = env.file.moduleName in + match Hashtbl.find_opt full.package.pathsForModule moduleName with + | None -> [] + | Some paths -> ( + let uri = getUri paths in + let cmt_path = getCmtPath ~uri paths in + match Shared.tryReadCmt cmt_path with + | None -> [] + | Some infos -> exceptions_from_cmt_infos infos) + in + let all = names_from_cmt @ predefined_exceptions in + all + |> List.map (fun (name, hasArgs) -> + let insertText = + if hasArgs then Printf.sprintf "throw(%s($0))" name + else Printf.sprintf "throw(%s)" name + in + let isBuiltin = List.mem (name, hasArgs) predefined_exceptions in + let detail = + if isBuiltin then "Built-in Exception" else "User-defined Exception" + in + Completion.create + (Printf.sprintf "throw(%s)" name) + ~env ~kind:(Completion.Value exn_typ) ~includesSnippets:true + ~insertText ~filterText:"throw" ~detail) + (** Completions intended for piping, from a completion path. *) let completionsForPipeFromCompletionPath ~envCompletionIsMadeFrom ~opens ~pos ~scope ~debug ~prefix ~env ~rawOpens ~full completionPath = @@ -1116,34 +1145,7 @@ and getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env ~exact in match (result, path) with | [], [prefix] when Utils.startsWith "throw" prefix -> - let exn_typ = Ctype.newconstr Predef.path_exn [] in - let names_from_cmt = - let moduleName = env.file.moduleName in - match Hashtbl.find_opt full.package.pathsForModule moduleName with - | None -> [] - | Some paths -> ( - let uri = getUri paths in - let cmt_path = getCmtPath ~uri paths in - match Shared.tryReadCmt cmt_path with - | None -> [] - | Some infos -> exceptions_from_cmt_infos infos) - in - let all = names_from_cmt @ predefined_exceptions in - all - |> List.map (fun (name, hasArgs) -> - let insertText = - if hasArgs then Printf.sprintf "throw(%s($0))" name - else Printf.sprintf "throw(%s)" name - in - let isBuiltin = List.mem (name, hasArgs) predefined_exceptions in - let detail = - if isBuiltin then "Built-in Exception" - else "User-defined Exception" - in - Completion.create - (Printf.sprintf "throw(%s)" name) - ~env ~kind:(Completion.Value exn_typ) ~includesSnippets:true - ~insertText ~filterText:"throw" ~detail) + completionsForThrow ~env ~full | _ -> result) | CPApply (cp, labels) -> ( if Debug.verbose () then print_endline "[ctx_path]--> CPApply"; From 5f1e834dd6d2396966f356951f85d2d3e6914298 Mon Sep 17 00:00:00 2001 From: nojaf Date: Wed, 24 Sep 2025 15:13:22 +0200 Subject: [PATCH 5/8] Use typed tree ast iterator --- analysis/src/CompletionBackEnd.ml | 75 +++++++++++++++++++------------ 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/analysis/src/CompletionBackEnd.ml b/analysis/src/CompletionBackEnd.ml index 61f7f0bf29..2f1696958c 100644 --- a/analysis/src/CompletionBackEnd.ml +++ b/analysis/src/CompletionBackEnd.ml @@ -775,40 +775,57 @@ let exceptions_from_cmt_infos (infos : Cmt_format.cmt_infos) : in Hashtbl.replace by_name name (prev || hasArgs) in - let rec of_structure_items (items : Typedtree.structure_item list) : unit = - match items with - | [] -> () - | item :: rest -> + (* Only collect top-level exception declarations (Tstr_exception/Tsig_exception). + Avoid picking up exceptions from Texp_letexception by tracking context. *) + let in_toplevel_exception = ref false in + let module Iter = TypedtreeIter.MakeIterator (struct + include TypedtreeIter.DefaultIteratorArgument + + let enter_structure_item (item : Typedtree.structure_item) = (match item.str_desc with - | Tstr_exception ext -> add_ext ext + | Tstr_exception _ -> in_toplevel_exception := true | _ -> ()); - of_structure_items rest - in - let rec of_signature_items (items : Typedtree.signature_item list) : unit = - match items with - | [] -> () - | item :: rest -> + () + + let leave_structure_item (_ : Typedtree.structure_item) = + in_toplevel_exception := false + + let enter_signature_item (item : Typedtree.signature_item) = (match item.sig_desc with - | Tsig_exception ext -> add_ext ext + | Tsig_exception _ -> in_toplevel_exception := true | _ -> ()); - of_signature_items rest - in - let of_parts (parts : Cmt_format.binary_part array) : unit = - Array.iter - (function - | Cmt_format.Partial_structure s -> of_structure_items s.str_items - | Partial_structure_item si -> of_structure_items [si] - | Partial_signature s -> of_signature_items s.sig_items - | Partial_signature_item si -> of_signature_items [si] - | _ -> ()) - parts + () + + let leave_signature_item (_ : Typedtree.signature_item) = + in_toplevel_exception := false + + let enter_extension_constructor (ext : Typedtree.extension_constructor) = + if !in_toplevel_exception then add_ext ext + end) in + let () = + match infos.cmt_annots with + | Cmt_format.Implementation s -> Iter.iter_structure s + | Interface s -> Iter.iter_signature s + | Partial_implementation parts -> + Array.iter + (function + | Cmt_format.Partial_structure s -> Iter.iter_structure s + | Partial_structure_item si -> Iter.iter_structure_item si + | Partial_signature s -> Iter.iter_signature s + | Partial_signature_item si -> Iter.iter_signature_item si + | _ -> ()) + parts + | Partial_interface parts -> + Array.iter + (function + | Cmt_format.Partial_structure s -> Iter.iter_structure s + | Partial_structure_item si -> Iter.iter_structure_item si + | Partial_signature s -> Iter.iter_signature s + | Partial_signature_item si -> Iter.iter_signature_item si + | _ -> ()) + parts + | _ -> () in - (match infos.cmt_annots with - | Cmt_format.Implementation s -> of_structure_items s.str_items - | Interface s -> of_signature_items s.sig_items - | Partial_implementation parts -> of_parts parts - | Partial_interface parts -> of_parts parts - | _ -> ()); Hashtbl.fold (fun name hasArgs acc -> (name, hasArgs) :: acc) by_name [] (* Predefined Stdlib/Pervasives exceptions. *) From 5cd2754f46b3078bb3914f7ab2c2da895a5aedf9 Mon Sep 17 00:00:00 2001 From: nojaf Date: Wed, 24 Sep 2025 15:34:34 +0200 Subject: [PATCH 6/8] Extract cmt helper to ProcessCmt --- analysis/src/CompletionBackEnd.ml | 80 ++----------------------------- analysis/src/ProcessCmt.ml | 74 ++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 77 deletions(-) diff --git a/analysis/src/CompletionBackEnd.ml b/analysis/src/CompletionBackEnd.ml index 2f1696958c..e15c6ea8a1 100644 --- a/analysis/src/CompletionBackEnd.ml +++ b/analysis/src/CompletionBackEnd.ml @@ -756,78 +756,6 @@ let getCompletionsForPath ~debug ~opens ~full ~pos ~exact ~scope findAllCompletions ~env ~prefix ~exact ~namesUsed ~completionContext | None -> [])) -(* Collect exception constructor names from cmt infos. *) -let exceptions_from_cmt_infos (infos : Cmt_format.cmt_infos) : - (string * bool) list = - let by_name : (string, bool) Hashtbl.t = Hashtbl.create 16 in - let add_ext (ext : Typedtree.extension_constructor) : unit = - let name = ext.ext_name.txt in - let hasArgs = - match ext.ext_kind with - | Text_decl (Cstr_tuple args, _ret) -> args <> [] - | Text_decl (Cstr_record fields, _ret) -> fields <> [] - | Text_rebind _ -> true - in - let prev = - match Hashtbl.find_opt by_name name with - | Some b -> b - | None -> false - in - Hashtbl.replace by_name name (prev || hasArgs) - in - (* Only collect top-level exception declarations (Tstr_exception/Tsig_exception). - Avoid picking up exceptions from Texp_letexception by tracking context. *) - let in_toplevel_exception = ref false in - let module Iter = TypedtreeIter.MakeIterator (struct - include TypedtreeIter.DefaultIteratorArgument - - let enter_structure_item (item : Typedtree.structure_item) = - (match item.str_desc with - | Tstr_exception _ -> in_toplevel_exception := true - | _ -> ()); - () - - let leave_structure_item (_ : Typedtree.structure_item) = - in_toplevel_exception := false - - let enter_signature_item (item : Typedtree.signature_item) = - (match item.sig_desc with - | Tsig_exception _ -> in_toplevel_exception := true - | _ -> ()); - () - - let leave_signature_item (_ : Typedtree.signature_item) = - in_toplevel_exception := false - - let enter_extension_constructor (ext : Typedtree.extension_constructor) = - if !in_toplevel_exception then add_ext ext - end) in - let () = - match infos.cmt_annots with - | Cmt_format.Implementation s -> Iter.iter_structure s - | Interface s -> Iter.iter_signature s - | Partial_implementation parts -> - Array.iter - (function - | Cmt_format.Partial_structure s -> Iter.iter_structure s - | Partial_structure_item si -> Iter.iter_structure_item si - | Partial_signature s -> Iter.iter_signature s - | Partial_signature_item si -> Iter.iter_signature_item si - | _ -> ()) - parts - | Partial_interface parts -> - Array.iter - (function - | Cmt_format.Partial_structure s -> Iter.iter_structure s - | Partial_structure_item si -> Iter.iter_structure_item si - | Partial_signature s -> Iter.iter_signature s - | Partial_signature_item si -> Iter.iter_signature_item si - | _ -> ()) - parts - | _ -> () - in - Hashtbl.fold (fun name hasArgs acc -> (name, hasArgs) :: acc) by_name [] - (* Predefined Stdlib/Pervasives exceptions. *) let predefined_exceptions : (string * bool) list = [ @@ -840,17 +768,15 @@ let predefined_exceptions : (string * bool) list = ] let completionsForThrow ~(env : QueryEnv.t) ~full = - let exn_typ = Ctype.newconstr Predef.path_exn [] in + let exn_typ = Predef.type_exn in let names_from_cmt = let moduleName = env.file.moduleName in match Hashtbl.find_opt full.package.pathsForModule moduleName with | None -> [] - | Some paths -> ( + | Some paths -> let uri = getUri paths in let cmt_path = getCmtPath ~uri paths in - match Shared.tryReadCmt cmt_path with - | None -> [] - | Some infos -> exceptions_from_cmt_infos infos) + ProcessCmt.exceptionsForCmt ~cmt:cmt_path in let all = names_from_cmt @ predefined_exceptions in all diff --git a/analysis/src/ProcessCmt.ml b/analysis/src/ProcessCmt.ml index 96601f6e3b..4852f1bf1c 100644 --- a/analysis/src/ProcessCmt.ml +++ b/analysis/src/ProcessCmt.ml @@ -793,3 +793,77 @@ let fileForModule moduleName ~package = | None -> Log.log ("No path for module " ^ moduleName); None + +(* Collect top-level exception constructors from typedtree/CMT file. *) +let exceptionsForCmt ~cmt : (string * bool) list = + match Shared.tryReadCmt cmt with + | None -> [] + | Some infos -> + let by_name : (string, bool) Hashtbl.t = Hashtbl.create 16 in + let add_ext (ext : Typedtree.extension_constructor) : unit = + let name = ext.ext_name.txt in + let hasArgs = + match ext.ext_kind with + | Text_decl (Cstr_tuple args, _ret) -> args <> [] + | Text_decl (Cstr_record fields, _ret) -> fields <> [] + | Text_rebind _ -> true + in + let prev = + match Hashtbl.find_opt by_name name with + | Some b -> b + | None -> false + in + Hashtbl.replace by_name name (prev || hasArgs) + in + (* Only collect top-level exception declarations (Tstr_exception/Tsig_exception). + Avoid picking up exceptions from Texp_letexception by tracking context. *) + let in_toplevel_exception = ref false in + let module Iter = TypedtreeIter.MakeIterator (struct + include TypedtreeIter.DefaultIteratorArgument + + let enter_structure_item (item : Typedtree.structure_item) = + (match item.str_desc with + | Tstr_exception _ -> in_toplevel_exception := true + | _ -> ()); + () + + let leave_structure_item (_ : Typedtree.structure_item) = + in_toplevel_exception := false + + let enter_signature_item (item : Typedtree.signature_item) = + (match item.sig_desc with + | Tsig_exception _ -> in_toplevel_exception := true + | _ -> ()); + () + + let leave_signature_item (_ : Typedtree.signature_item) = + in_toplevel_exception := false + + let enter_extension_constructor (ext : Typedtree.extension_constructor) = + if !in_toplevel_exception then add_ext ext + end) in + let () = + match infos.cmt_annots with + | Cmt_format.Implementation s -> Iter.iter_structure s + | Interface s -> Iter.iter_signature s + | Partial_implementation parts -> + Array.iter + (function + | Cmt_format.Partial_structure s -> Iter.iter_structure s + | Partial_structure_item si -> Iter.iter_structure_item si + | Partial_signature s -> Iter.iter_signature s + | Partial_signature_item si -> Iter.iter_signature_item si + | _ -> ()) + parts + | Partial_interface parts -> + Array.iter + (function + | Cmt_format.Partial_structure s -> Iter.iter_structure s + | Partial_structure_item si -> Iter.iter_structure_item si + | Partial_signature s -> Iter.iter_signature s + | Partial_signature_item si -> Iter.iter_signature_item si + | _ -> ()) + parts + | _ -> () + in + Hashtbl.fold (fun name hasArgs acc -> (name, hasArgs) :: acc) by_name [] From d23722d6b0b61e0784e2a685cc906866e51dc5a8 Mon Sep 17 00:00:00 2001 From: nojaf Date: Wed, 24 Sep 2025 17:23:56 +0200 Subject: [PATCH 7/8] Remove ReScript exceptinons and add hardcoded throw functions. --- analysis/src/CompletionBackEnd.ml | 46 ++++++-------- .../tests/src/expected/Throw.res.txt | 62 ++++--------------- 2 files changed, 32 insertions(+), 76 deletions(-) diff --git a/analysis/src/CompletionBackEnd.ml b/analysis/src/CompletionBackEnd.ml index e15c6ea8a1..13590fc90d 100644 --- a/analysis/src/CompletionBackEnd.ml +++ b/analysis/src/CompletionBackEnd.ml @@ -756,17 +756,6 @@ let getCompletionsForPath ~debug ~opens ~full ~pos ~exact ~scope findAllCompletions ~env ~prefix ~exact ~namesUsed ~completionContext | None -> [])) -(* Predefined Stdlib/Pervasives exceptions. *) -let predefined_exceptions : (string * bool) list = - [ - ("Not_found", true); - ("Invalid_argument", true); - ("Assert_failure", true); - ("Failure", true); - ("Match_failure", true); - ("Division_by_zero", false); - ] - let completionsForThrow ~(env : QueryEnv.t) ~full = let exn_typ = Predef.type_exn in let names_from_cmt = @@ -778,21 +767,26 @@ let completionsForThrow ~(env : QueryEnv.t) ~full = let cmt_path = getCmtPath ~uri paths in ProcessCmt.exceptionsForCmt ~cmt:cmt_path in - let all = names_from_cmt @ predefined_exceptions in - all - |> List.map (fun (name, hasArgs) -> - let insertText = - if hasArgs then Printf.sprintf "throw(%s($0))" name - else Printf.sprintf "throw(%s)" name - in - let isBuiltin = List.mem (name, hasArgs) predefined_exceptions in - let detail = - if isBuiltin then "Built-in Exception" else "User-defined Exception" - in - Completion.create - (Printf.sprintf "throw(%s)" name) - ~env ~kind:(Completion.Value exn_typ) ~includesSnippets:true - ~insertText ~filterText:"throw" ~detail) + let completions_from_cmt = + names_from_cmt + |> List.map (fun (name, hasArgs) -> + let insertText = + if hasArgs then Printf.sprintf "throw(%s($0))" name + else Printf.sprintf "throw(%s)" name + in + Completion.create + (Printf.sprintf "throw(%s)" name) + ~env ~kind:(Completion.Value exn_typ) ~includesSnippets:true + ~insertText ~filterText:"throw") + in + Completion.create "JsError.throwWithMessage" ~env + ~kind:(Completion.Value exn_typ) ~includesSnippets:true + ~detail:"Throw a JavaScript error, example: `throw new Error(str)`" + ~insertText:"JsError.throwWithMessage(\"$0\")" + :: Completion.create "JsExn.throw" ~env ~kind:(Completion.Value exn_typ) + ~includesSnippets:true ~insertText:"JsExn.throw($0)" + ~detail:"Throw any JavaScript value, example: `throw 100`" + :: completions_from_cmt (** Completions intended for piping, from a completion path. *) let completionsForPipeFromCompletionPath ~envCompletionIsMadeFrom ~opens ~pos diff --git a/tests/analysis_tests/tests/src/expected/Throw.res.txt b/tests/analysis_tests/tests/src/expected/Throw.res.txt index 7971c5a665..7070e60c51 100644 --- a/tests/analysis_tests/tests/src/expected/Throw.res.txt +++ b/tests/analysis_tests/tests/src/expected/Throw.res.txt @@ -8,76 +8,38 @@ Resolved opens 1 Stdlib ContextPath Value[thro] Path thro [{ - "label": "throw(MyCustomThingToThrow)", - "kind": 12, - "tags": [], - "detail": "User-defined Exception", - "documentation": null, - "filterText": "throw", - "insertText": "throw(MyCustomThingToThrow($0))", - "insertTextFormat": 2 - }, { - "label": "throw(NoArgsToThrow)", - "kind": 12, - "tags": [], - "detail": "User-defined Exception", - "documentation": null, - "filterText": "throw", - "insertText": "throw(NoArgsToThrow)", - "insertTextFormat": 2 - }, { - "label": "throw(Not_found)", - "kind": 12, - "tags": [], - "detail": "Built-in Exception", - "documentation": null, - "filterText": "throw", - "insertText": "throw(Not_found($0))", - "insertTextFormat": 2 - }, { - "label": "throw(Invalid_argument)", + "label": "JsError.throwWithMessage", "kind": 12, "tags": [], - "detail": "Built-in Exception", + "detail": "Throw a JavaScript error, example: `throw new Error(str)`", "documentation": null, - "filterText": "throw", - "insertText": "throw(Invalid_argument($0))", + "insertText": "JsError.throwWithMessage(\"$0\")", "insertTextFormat": 2 }, { - "label": "throw(Assert_failure)", + "label": "JsExn.throw", "kind": 12, "tags": [], - "detail": "Built-in Exception", + "detail": "Throw any JavaScript value, example: throw 100", "documentation": null, - "filterText": "throw", - "insertText": "throw(Assert_failure($0))", + "insertText": "JsExn.throw($0)", "insertTextFormat": 2 }, { - "label": "throw(Failure)", - "kind": 12, - "tags": [], - "detail": "Built-in Exception", - "documentation": null, - "filterText": "throw", - "insertText": "throw(Failure($0))", - "insertTextFormat": 2 - }, { - "label": "throw(Match_failure)", + "label": "throw(MyCustomThingToThrow)", "kind": 12, "tags": [], - "detail": "Built-in Exception", + "detail": "exn", "documentation": null, "filterText": "throw", - "insertText": "throw(Match_failure($0))", + "insertText": "throw(MyCustomThingToThrow($0))", "insertTextFormat": 2 }, { - "label": "throw(Division_by_zero)", + "label": "throw(NoArgsToThrow)", "kind": 12, "tags": [], - "detail": "Built-in Exception", + "detail": "exn", "documentation": null, "filterText": "throw", - "insertText": "throw(Division_by_zero)", + "insertText": "throw(NoArgsToThrow)", "insertTextFormat": 2 }] From 7dc6ff16608296b062cbdecf7092e6ce7f97535e Mon Sep 17 00:00:00 2001 From: nojaf Date: Fri, 26 Sep 2025 08:33:16 +0200 Subject: [PATCH 8/8] Update snapshot --- tests/analysis_tests/tests/src/expected/Throw.res.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/analysis_tests/tests/src/expected/Throw.res.txt b/tests/analysis_tests/tests/src/expected/Throw.res.txt index 7070e60c51..443bdf9229 100644 --- a/tests/analysis_tests/tests/src/expected/Throw.res.txt +++ b/tests/analysis_tests/tests/src/expected/Throw.res.txt @@ -19,7 +19,7 @@ Path thro "label": "JsExn.throw", "kind": 12, "tags": [], - "detail": "Throw any JavaScript value, example: throw 100", + "detail": "Throw any JavaScript value, example: `throw 100`", "documentation": null, "insertText": "JsExn.throw($0)", "insertTextFormat": 2