Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#### :rocket: New Feature

- Reanalyze: add scoped `@@live`/`@@dead` annotations for marking module/file sections as live or dead. https://github.com/rescript-lang/rescript/pull/8197
- Add completions for `throw`. https://github.com/rescript-lang/rescript/pull/7905

#### :bug: Bug fix

Expand Down
241 changes: 154 additions & 87 deletions analysis/src/CompletionBackEnd.ml
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,50 @@ let getCompletionsForPath ~debug ~opens ~full ~pos ~exact ~scope
findAllCompletions ~env ~prefix ~exact ~namesUsed ~completionContext
| None -> []))

let getExceptionNamesFromCmt ~(env : QueryEnv.t) ~full =
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
ProcessCmt.exceptionsForCmt ~cmt:cmt_path

let completionsForThrowArg ~(env : QueryEnv.t) ~full =
let exn_typ = Predef.type_exn in
let names_from_cmt = getExceptionNamesFromCmt ~env ~full in
names_from_cmt
|> List.map (fun (name, hasArgs) ->
let insertText =
if hasArgs then Printf.sprintf "%s($0)" name else name
in
Completion.create name ~env ~kind:(Completion.Value exn_typ)
~includesSnippets:hasArgs ~insertText)

let completionsForThrow ~(env : QueryEnv.t) ~full =
let exn_typ = Predef.type_exn in
let names_from_cmt = getExceptionNamesFromCmt ~env ~full in
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
~scope ~debug ~prefix ~env ~rawOpens ~full completionPath =
Expand Down Expand Up @@ -1010,7 +1054,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; completionContext; loc} ->
| CPId {path; completionContext; loc} -> (
if Debug.verbose () then print_endline "[ctx_path]--> CPId";
(* Looks up the type of an identifier.

Expand Down Expand Up @@ -1048,7 +1092,10 @@ and getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env ~exact
| _ -> byPath
else byPath
in
result
match (result, path) with
| [], [prefix] when Utils.startsWith "throw" prefix ->
completionsForThrow ~env ~full
| _ -> result)
Comment on lines +1095 to +1098
Copy link
Member

Choose a reason for hiding this comment

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

This should probably be done in a "safer" way that matches by type and not by name. We'd need to check if this thing actually resolves to Pervasives.throw.

Also, why Utils.startsWith? Is it not supposed to be an exact match?

Copy link
Member

Choose a reason for hiding this comment

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

@nojaf this has not been answered I believe.

Copy link
Member Author

Choose a reason for hiding this comment

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

We'd need to check if this thing actually resolves to Pervasives.throw.

If you only have thr you can't do that.

Also, why Utils.startsWith? Is it not supposed to be an exact match?

As mentioned, I want the completions to show up while I'm typing towards the word throw. Only having it once the full word is typed I find unintuitive.

| CPApply (cp, labels) -> (
if Debug.verbose () then print_endline "[ctx_path]--> CPApply";
match
Expand Down Expand Up @@ -2283,105 +2330,125 @@ let rec processCompletable ~debug ~full ~scope ~env ~pos ~forHover completable =
fallbackOrEmpty ~items ())
| None -> fallbackOrEmpty ())
| Cexpression {contextPath; prefix; nested} -> (
let isAmbigiousRecordBodyOrJsxWrap =
match (contextPath, nested) with
| CJsxPropValue _, [NRecordBody _] -> true
(* Special case: completing argument to throw() should return exception constructors *)
let isThrowArg =
match contextPath with
| CArgument
{
functionContextPath =
CPId {path = ["throw"]; completionContext = Value};
argumentLabel = Unlabelled _;
} ->
true
| _ -> false
in
if Debug.verbose () then
(* This happens in this scenario: `<SomeComponent someProp={<com>}`
Here, we don't know whether `{}` is just wraps for the type of
`someProp`, or if it's a record body where we want to complete
for the fields in the record. We need to look up what the type is
first before deciding what completions to show. So we do that here.*)
if isAmbigiousRecordBodyOrJsxWrap then
print_endline
"[process_completable]--> Cexpression special case: JSX prop value \
that might be record body or JSX wrap"
else print_endline "[process_completable]--> Cexpression";
(* Completions for local things like variables in scope, modules in the
if isThrowArg then
completionsForThrowArg ~env ~full
|> List.filter (fun (c : Completion.t) ->
prefix = "" || Utils.startsWith c.name prefix)
else
let isAmbigiousRecordBodyOrJsxWrap =
match (contextPath, nested) with
| CJsxPropValue _, [NRecordBody _] -> true
| _ -> false
in
if Debug.verbose () then
(* This happens in this scenario: `<SomeComponent someProp={<com>}`
Here, we don't know whether `{}` is just wraps for the type of
`someProp`, or if it's a record body where we want to complete
for the fields in the record. We need to look up what the type is
first before deciding what completions to show. So we do that here.*)
if isAmbigiousRecordBodyOrJsxWrap then
print_endline
"[process_completable]--> Cexpression special case: JSX prop value \
that might be record body or JSX wrap"
else print_endline "[process_completable]--> Cexpression";
(* Completions for local things like variables in scope, modules in the
project, etc. We only add completions when there's a prefix of some sort
we can filter on, since we know we're in some sort of context, and
therefore don't want to overwhelm the user with completion items. *)
let regularCompletions =
if prefix = "" then []
else
prefix
|> getComplementaryCompletionsForTypedValue ~opens ~allFiles ~env ~scope
in
match
contextPath
|> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env
~exact:true ~scope
|> completionsGetCompletionType ~full
with
| None ->
if Debug.verbose () then
print_endline
"[process_completable]--> could not get completions for context path";
regularCompletions
| Some (typ, env) -> (
match typ |> TypeUtils.resolveNested ~env ~full ~nested with
let regularCompletions =
if prefix = "" then []
else
prefix
|> getComplementaryCompletionsForTypedValue ~opens ~allFiles ~env
~scope
in
match
contextPath
|> getCompletionsForContextPath ~debug ~full ~opens ~rawOpens ~pos ~env
~exact:true ~scope
|> completionsGetCompletionType ~full
with
| None ->
if Debug.verbose () then
print_endline
"[process_completable]--> could not resolve nested expression path";
if isAmbigiousRecordBodyOrJsxWrap then (
"[process_completable]--> could not get completions for context \
path";
regularCompletions
| Some (typ, env) -> (
match typ |> TypeUtils.resolveNested ~env ~full ~nested with
| None ->
if Debug.verbose () then
print_endline
"[process_completable]--> case is ambigious Jsx prop vs record \
body case, complete also for the JSX prop value directly";
let itemsForRawJsxPropValue =
typ
|> completeTypedValue ~rawOpens ~mode:Expression ~full ~prefix
~completionContext:None
in
itemsForRawJsxPropValue @ regularCompletions)
else regularCompletions
| Some (typ, _env, completionContext, typeArgContext) -> (
if Debug.verbose () then
print_endline
"[process_completable]--> found type in nested expression \
completion";
(* Wrap the insert text in braces when we're completing the root of a
"[process_completable]--> could not resolve nested expression \
path";
if isAmbigiousRecordBodyOrJsxWrap then (
if Debug.verbose () then
print_endline
"[process_completable]--> case is ambigious Jsx prop vs record \
body case, complete also for the JSX prop value directly";
let itemsForRawJsxPropValue =
typ
|> completeTypedValue ~rawOpens ~mode:Expression ~full ~prefix
~completionContext:None
in
itemsForRawJsxPropValue @ regularCompletions)
else regularCompletions
| Some (typ, _env, completionContext, typeArgContext) -> (
if Debug.verbose () then
print_endline
"[process_completable]--> found type in nested expression \
completion";
(* Wrap the insert text in braces when we're completing the root of a
JSX prop value. *)
let wrapInsertTextInBraces =
if List.length nested > 0 then false
else
match contextPath with
| CJsxPropValue _ -> true
| _ -> false
in
let items =
typ
|> completeTypedValue ?typeArgContext ~rawOpens ~mode:Expression ~full
~prefix ~completionContext
|> List.map (fun (c : Completion.t) ->
if wrapInsertTextInBraces then
{
c with
insertText =
(match c.insertText with
| None -> None
| Some text -> Some ("{" ^ text ^ "}"));
}
else c)
in
match (prefix, completionContext) with
| "", _ -> items
| _, None ->
let wrapInsertTextInBraces =
if List.length nested > 0 then false
else
match contextPath with
| CJsxPropValue _ -> true
| _ -> false
in
let items =
if List.length regularCompletions > 0 then
(* The client will occasionally sort the list of completions alphabetically, disregarding the order
typ
|> completeTypedValue ?typeArgContext ~rawOpens ~mode:Expression
~full ~prefix ~completionContext
|> List.map (fun (c : Completion.t) ->
if wrapInsertTextInBraces then
{
c with
insertText =
(match c.insertText with
| None -> None
| Some text -> Some ("{" ^ text ^ "}"));
}
else c)
in
match (prefix, completionContext) with
| "", _ -> items
| _, None ->
let items =
if List.length regularCompletions > 0 then
(* The client will occasionally sort the list of completions alphabetically, disregarding the order
in which we send it. This fixes that by providing a sort text making the typed completions
guaranteed to end up on top. *)
items
|> List.map (fun (c : Completion.t) ->
{c with sortText = Some ("A" ^ " " ^ c.name)})
else items
in
items @ regularCompletions
| _ -> items)))
items
|> List.map (fun (c : Completion.t) ->
{c with sortText = Some ("A" ^ " " ^ c.name)})
else items
in
items @ regularCompletions
| _ -> items)))
| CexhaustiveSwitch {contextPath; exprLoc} ->
let range = Utils.rangeOfLoc exprLoc in
let printFailwithStr num = "${" ^ string_of_int num ^ ":%todo}" in
Expand Down
72 changes: 72 additions & 0 deletions analysis/src/ProcessCmt.ml
Original file line number Diff line number Diff line change
Expand Up @@ -793,3 +793,75 @@ 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. *)
Comment on lines +818 to +819
Copy link
Member

Choose a reason for hiding this comment

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

In theory I guess the most robust approach would be to collect and use whatever defined exceptions are in scope at the current position. Collecting at the top level is good and simple, but you could also define them in sub modules, and so on.

It might be worth exploring making exceptions part of the tracked scope instead, and track them just like we track types and values today when doing completion. Then we could just extend the current mechanism a bit, and hopefully get the correct scope tracking for free.

let in_toplevel_exception = ref false in
let module Iter = TypedtreeIter.MakeIterator (struct
include TypedtreeIter.DefaultIteratorArgument
Comment on lines +821 to +822
Copy link
Member

Choose a reason for hiding this comment

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

Do we use the functor based approach elsewhere in the code base? Or is there no way for the typed tree to just define a mapper like we do for the AST mapper? I don't remember how this works exactly.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, this was weird to me as well, it is a departure from what we have on the untyped side, but this seems to be the mapper we have?


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 []
11 changes: 11 additions & 0 deletions tests/analysis_tests/tests/src/Throw.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
exception MyCustomThingToThrow(string)
exception NoArgsToThrow

// let x = () => thro
// ^com

// let y = () => throw(MyCu)
// ^com

// let z = () => throw()
// ^com
10 changes: 10 additions & 0 deletions tests/analysis_tests/tests/src/expected/Completion.res.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2153,6 +2153,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,
Expand Down
Loading
Loading