From e751edbe5ff9921bb2d93ef8b09141bb907229a0 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Tue, 27 Oct 2026 13:06:34 +0000 Subject: [PATCH 01/10] Complete the constructors of extensible variants Why: Editor analysis didn't record the constructors added by `type t += ...`, and an open type has none in its declaration, so completion offered no constructor where a value of the type was expected. They are now registered as constructors of an open type, and typed completion of an open type offers those declared in the type's file and in the current file. Since other files can extend the type, the exhaustive switch snippet for one adds a `_` case. Diagnostic modified (completion of an extensible variant): Example: type attr = .. type attr += Str(string) | Num(int) // let a: attr = Nu Before (text): tests/analysis_tests/tests/src/CompletionExtensibleVariant.res Nullable, Null After (text): tests/analysis_tests/tests/src/CompletionExtensibleVariant.res Num(_), Nullable, Null Diagnostic modified (exhaustive switch snippet): Example: let value: attr = Str("s") // switch valu After (rescript): switch value { | Str(_) => %todo | Num(_) => %todo | Flag => %todo | _ => %todo } Review: Which extensions are offered? Those in the file that declares the type and in the current file; an extension in a third file is not known to the current file's analysis. Fixes: #9073 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3 Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo Signed-off-by: Christoph Knittel --- analysis/src/completion_back_end.ml | 26 ++++--- analysis/src/process_cmt.ml | 60 ++++++++++++++++ analysis/src/type_utils.ml | 54 +++++++++++++++ changes/extensible-variant-completion.fix.md | 1 + .../tests/src/CompletionExtensibleVariant.res | 17 +++++ .../CompletionExtensibleVariant.res.txt | 68 +++++++++++++++++++ 6 files changed, 217 insertions(+), 9 deletions(-) create mode 100644 changes/extensible-variant-completion.fix.md create mode 100644 tests/analysis_tests/tests/src/CompletionExtensibleVariant.res create mode 100644 tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt diff --git a/analysis/src/completion_back_end.ml b/analysis/src/completion_back_end.ml index 2ebd180482..d940c97fdf 100644 --- a/analysis/src/completion_back_end.ml +++ b/analysis/src/completion_back_end.ml @@ -2569,15 +2569,23 @@ let rec process_completable ~state ~debug ~full ~scope ~env ~pos ~for_hover typ_expr |> Type_utils.extract_type ~env:c.env ~package ~state with | Some (Tvariant v, _) -> - with_exhaustive_item c - ~cases: - (v.constructors - |> List.map (fun (constructor : Constructor.t) -> - constructor.cname.txt - ^ - match constructor.args with - | Args [] -> "" - | _ -> "(_)")) + let cases = + v.constructors + |> List.map (fun (constructor : Constructor.t) -> + constructor.cname.txt + ^ + match constructor.args with + | Args [] -> "" + | _ -> "(_)") + in + (* An open type can be extended elsewhere, so only a catch-all + case makes the switch exhaustive. *) + let cases = + match v.variant_decl.type_kind with + | Type_open -> cases @ ["_"] + | _ -> cases + in + with_exhaustive_item c ~cases | Some (Tpolyvariant v, _) -> with_exhaustive_item c ~cases: diff --git a/analysis/src/process_cmt.ml b/analysis/src/process_cmt.ml index 18d854a524..6cd43d3782 100644 --- a/analysis/src/process_cmt.ml +++ b/analysis/src/process_cmt.ml @@ -328,6 +328,60 @@ let for_type_declaration ~env ~(exported : Exported.t) loc = declared.extent_loc; } +(* The constructors of [type t += ...] are registered as constructors of an + open type named [t], so that completion can offer them for values of + type [t]. They are not part of any type declaration, hence the stand-in + declaration that only carries the kind. *) +let add_extension_constructors ~(env : Env.t) + {Typedtree.tyext_path; tyext_constructors} = + let type_name = Path.last tyext_path in + let type_decl : Types.type_declaration = + { + type_params = []; + type_arity = 0; + type_kind = Type_open; + type_private = Public; + type_manifest = None; + type_variance = []; + type_newtype_level = None; + type_loc = Location.none; + type_attributes = []; + type_immediate = false; + type_representation = Boxed; + type_shape = Unstated; + type_inlined_types = []; + } + in + tyext_constructors + |> List.iter + (fun + ({ext_id; ext_name; ext_type; ext_loc; ext_attributes} : + Typedtree.extension_constructor) + -> + let stamp = Ident.binding_time ext_id in + let item = + { + Constructor.stamp; + cname = ext_name; + args = + (match ext_type.ext_args with + | Cstr_tuple args -> + Args (args |> List.map (fun t -> (t, Location.none))) + | Cstr_record fields -> + InlineRecord (fields |> List.map map_record_field)); + res = ext_type.ext_ret_type; + type_decl = (type_name, type_decl); + docstring = attrs_to_docstring ext_attributes; + deprecated = + Process_attributes.find_deprecated_attribute ext_attributes; + } + in + let declared = + Process_attributes.new_declared ~item ~extent:ext_loc ~name:ext_name + ~stamp ~module_path:env.module_path true ext_attributes + in + Stamps.add_constructor env.stamps stamp declared) + let rec for_signature_item ~env ~(exported : Exported.t) (item : Typedtree.signature_item) = match item.sig_desc with @@ -401,6 +455,9 @@ let rec for_signature_item ~env ~(exported : Exported.t) incl_type [] in top_level + | Tsig_typext type_extension -> + add_extension_constructors ~env type_extension; + [] (* TODO: process other things here *) | _ -> [] @@ -645,6 +702,9 @@ let rec for_structure_item ~(env : Shared_types.Env.t) ~(exported : Exported.t) | _ -> Types.Trec_next in decl |> for_type_declaration ~env ~exported ~rec_status) + | Tstr_typext type_extension -> + add_extension_constructors ~env type_extension; + [] | _ -> [] and for_module ~env mod_desc module_name = diff --git a/analysis/src/type_utils.ml b/analysis/src/type_utils.ml index 0ad1ba8411..cbc69a323e 100644 --- a/analysis/src/type_utils.ml +++ b/analysis/src/type_utils.ml @@ -361,6 +361,27 @@ let extract_function_type2 ?type_arg_context ~state ~env ~package typ = in loop ?type_arg_context ~env [] typ +(* The constructors of an open type are those of its extensions, here the + ones in the given files. *) +let extension_constructors ~(files : File.t list) ~type_name ~decl = + let found = Hashtbl.create 8 in + files + |> List.iter (fun (file : File.t) -> + file.stamps + |> Stamps.iter_constructors + (fun stamp (declared : Constructor.t Declared.t) -> + let name, ext_decl = declared.item.type_decl in + if name = type_name && ext_decl.type_kind = Type_open then + Hashtbl.replace found (file.uri, stamp) declared.item)); + Hashtbl.to_seq_values found + |> List.of_seq + |> List.sort (fun (a : Constructor.t) (b : Constructor.t) -> + compare + (a.cname.loc.loc_start.pos_fname, a.cname.loc.loc_start.pos_cnum) + (b.cname.loc.loc_start.pos_fname, b.cname.loc.loc_start.pos_cnum)) + |> List.map (fun (c : Constructor.t) -> + {c with type_decl = (type_name, decl)}) + let rec extract_type ?(print_opening_debug = true) ?(type_arg_context : type_arg_context option) ?(type_arg_context_from_type_manifest : type_arg_context option) ~env ~state @@ -445,6 +466,28 @@ let rec extract_type ?(print_opening_debug = true) variant_decl = decl; }, type_arg_context ) + | Some (env_from_item, {name; item = {decl; kind = Type.Open}}) + when decl.type_manifest = None -> ( + let type_arg_context = + maybe_set_type_arg_ctx ~type_params:decl.type_params ~type_args env + in + match + extension_constructors ~files:[env_from_item.file] ~type_name:name.txt + ~decl + with + | [] when name.txt = "t" -> + Some (TtypeT {env = env_from_item; path}, type_arg_context) + | [] -> None + | constructors -> + Some + ( Tvariant + { + env = env_from_item; + constructors; + variant_name = name.txt; + variant_decl = decl; + }, + type_arg_context )) | Some (env_from_declaration, {item = {kind = Record fields; decl}}) -> if Debug.verbose () then print_endline "[extract_type]--> found record"; (* Need to create a new type arg context here because we're sending along a type expr that might have type vars. *) @@ -601,6 +644,17 @@ let extract_type_from_resolved_type (typ : Type.t) ~env ~full ~state = Some (Tvariant {env; constructors; variant_name = typ.name; variant_decl = typ.decl}) + | Open when typ.decl.type_manifest = None -> ( + match + extension_constructors ~files:[env.file; full.file] ~type_name:typ.name + ~decl:typ.decl + with + | [] -> None + | constructors -> + Some + (Tvariant + {env; constructors; variant_name = typ.name; variant_decl = typ.decl}) + ) | Abstract _ | Open -> ( match typ.decl.type_manifest with | None -> None diff --git a/changes/extensible-variant-completion.fix.md b/changes/extensible-variant-completion.fix.md new file mode 100644 index 0000000000..cf6be35047 --- /dev/null +++ b/changes/extensible-variant-completion.fix.md @@ -0,0 +1 @@ +- Editor completion offers the constructors of an extensible variant declared in the current file or in the type's file, and the exhaustive switch snippet for one ends with a `_` case. diff --git a/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res b/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res new file mode 100644 index 0000000000..6d79c41e53 --- /dev/null +++ b/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res @@ -0,0 +1,17 @@ +type attr = .. +type attr += Str(string) | Num(int) + +module Sub = { + type attr += Flag +} + +// let a: attr = Nu +// ^com + +let value: attr = Str("s") + +// switch value { | S +// ^com + +// switch valu +// ^com diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt new file mode 100644 index 0000000000..c143e2f36d --- /dev/null +++ b/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt @@ -0,0 +1,68 @@ +Complete src/CompletionExtensibleVariant.res 7:19 +XXX Not found! +Completable: Cexpression Type[attr]=Nu +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Type[attr] +Path attr +[ + { + "detail": "Num(int)", + "documentation": { + "kind": "markdown", + "value": "```rescript\nNum(int)\n```\n\n```rescript\ntype attr = ..\n```" + }, + "insertText": "Num($0)", + "insertTextFormat": 2, + "kind": 4, + "label": "Num(_)", + "sortText": "A Num(_)", + "tags": [] + }, + { "detail": "module Nullable", "kind": 9, "label": "Nullable", "tags": [] }, + { "detail": "module Null", "kind": 9, "label": "Null", "tags": [] } +] + +Complete src/CompletionExtensibleVariant.res 12:21 +pos_cursor:[12:21] pos_no_white:[12:20] Found pattern:[12:20->12:21] +Ppat_construct S:[12:20->12:21] +Completable: Cpattern Value[value]=S +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[value] +Path value +[ + { + "detail": "Str(string)", + "documentation": { + "kind": "markdown", + "value": "```rescript\nStr(string)\n```\n\n```rescript\ntype attr = ..\n```" + }, + "insertText": "Str(${1:_})", + "insertTextFormat": 2, + "kind": 4, + "label": "Str(_)", + "tags": [] + } +] + +Complete src/CompletionExtensibleVariant.res 15:14 +XXX Not found! +Completable: CexhaustiveSwitch Value[valu] +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[valu] +Path valu +[ + { "detail": "attr", "kind": 12, "label": "value", "tags": [] }, + { + "detail": "insert exhaustive switch for value", + "filterText": "value", + "insertText": "value {\n | Str(_) => ${1:%todo}\n | Num(_) => ${2:%todo}\n | Flag => ${3:%todo}\n | _ => ${4:%todo}\n }", + "insertTextFormat": 2, + "kind": 15, + "label": "value (exhaustive switch)", + "tags": [] + } +] + From 5639dff6bd1bfcf830a177b644bc48585871bf22 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Tue, 27 Oct 2026 17:50:11 +0000 Subject: [PATCH 02/10] Match extension constructors by type and name them by module path Codex review on #9085: extensions were matched to an open type by its last name only, so `A.t` got the constructors of `B.t`, and a constructor declared in a nested module was inserted without its module, which doesn't compile because an extension constructor can't be found from the expected type. An extension now records the type it extends and is matched by that type's file and stamp, and it is named by its module path, for example `Sub.Flag`; a prefix may match the last part. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3 Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo Signed-off-by: Christoph Knittel --- analysis/src/completion_back_end.ml | 16 ++- analysis/src/process_cmt.ml | 5 +- analysis/src/type_utils.ml | 83 +++++++++++++--- .../tests/src/CompletionExtensibleVariant.res | 23 +++++ .../CompletionExtensibleVariant.res.txt | 99 ++++++++++++++++++- 5 files changed, 203 insertions(+), 23 deletions(-) diff --git a/analysis/src/completion_back_end.ml b/analysis/src/completion_back_end.ml index d940c97fdf..d2de91a4ba 100644 --- a/analysis/src/completion_back_end.ml +++ b/analysis/src/completion_back_end.ml @@ -1783,7 +1783,22 @@ let rec complete_typed_value ?(type_arg_context : type_arg_context option) ] | Tvariant {env; constructors; variant_decl; variant_name} -> if Debug.verbose () then print_endline "[complete_typed_value]--> Tvariant"; + (* An extension constructor is named by its module path, as in + [Sub.Flag]; the prefix may also be the start of its last part. *) + let matches_prefix name = + prefix = "" + || Utils.starts_with name prefix + || + match String.rindex_opt name '.' with + | Some i -> + Utils.starts_with + (String.sub name (i + 1) (String.length name - i - 1)) + prefix + | None -> false + in constructors + |> List.filter (fun (constructor : Constructor.t) -> + matches_prefix constructor.cname.txt) |> List.map (fun (constructor : Constructor.t) -> let num_args = match constructor.args with @@ -1800,7 +1815,6 @@ let rec complete_typed_value ?(type_arg_context : type_arg_context option) (Constructor (constructor, variant_decl |> Shared.decl_to_string variant_name)) ~env) - |> filter_items ~prefix | Tpolyvariant {env; constructors; type_expr} -> if Debug.verbose () then print_endline "[complete_typed_value]--> Tpolyvariant"; diff --git a/analysis/src/process_cmt.ml b/analysis/src/process_cmt.ml index 6cd43d3782..270b4b2b8b 100644 --- a/analysis/src/process_cmt.ml +++ b/analysis/src/process_cmt.ml @@ -331,7 +331,8 @@ let for_type_declaration ~env ~(exported : Exported.t) (* The constructors of [type t += ...] are registered as constructors of an open type named [t], so that completion can offer them for values of type [t]. They are not part of any type declaration, hence the stand-in - declaration that only carries the kind. *) + declaration that carries the kind and, as its manifest, the extended + type, which completion compares with the type it completes. *) let add_extension_constructors ~(env : Env.t) {Typedtree.tyext_path; tyext_constructors} = let type_name = Path.last tyext_path in @@ -341,7 +342,7 @@ let add_extension_constructors ~(env : Env.t) type_arity = 0; type_kind = Type_open; type_private = Public; - type_manifest = None; + type_manifest = Some (Ctype.newconstr tyext_path []); type_variance = []; type_newtype_level = None; type_loc = Location.none; diff --git a/analysis/src/type_utils.ml b/analysis/src/type_utils.ml index cbc69a323e..7e0efeab46 100644 --- a/analysis/src/type_utils.ml +++ b/analysis/src/type_utils.ml @@ -361,26 +361,77 @@ let extract_function_type2 ?type_arg_context ~state ~env ~package typ = in loop ?type_arg_context ~env [] typ +(* The stamp under which [typ] is declared in [file]. *) +let type_stamp (file : File.t) (typ : Type.t) = + let found = ref None in + file.stamps + |> Stamps.iter_types (fun stamp (declared : Type.t Declared.t) -> + if declared.item == typ then found := Some stamp); + !found + (* The constructors of an open type are those of its extensions, here the - ones in the given files. *) -let extension_constructors ~(files : File.t list) ~type_name ~decl = + ones in the type's file and in the current file. An extension names the + type it extends by its path where it is declared: an identifier of the + type's own file, or a path resolved from the extension's file. The + constructors are named by their module path, since an extension + constructor can't be found from the expected type alone. *) +let extension_constructors ~state ~package ~(current_file : File.t) + ~(type_file : File.t) ~type_stamp ~type_name ~decl = + let extends (file : File.t) (ext_decl : Types.type_declaration) = + match (type_stamp, ext_decl.type_manifest) with + | None, _ -> true + | Some stamp, Some {desc = Tconstr (Pident id, _, _)} -> + file.uri = type_file.uri && Ident.binding_time id = stamp + | Some stamp, Some {desc = Tconstr (path, _, _)} -> ( + match + References.dig_constructor ~state ~env:(Query_env.from_file file) + ~package path + with + | Some (env, declared) -> + env.file.uri = type_file.uri && declared.stamp = stamp + | None -> false) + | Some _, _ -> false + in let found = Hashtbl.create 8 in + let files = + if current_file.uri = type_file.uri then [type_file] + else [type_file; current_file] + in files |> List.iter (fun (file : File.t) -> file.stamps |> Stamps.iter_constructors (fun stamp (declared : Constructor.t Declared.t) -> let name, ext_decl = declared.item.type_decl in - if name = type_name && ext_decl.type_kind = Type_open then - Hashtbl.replace found (file.uri, stamp) declared.item)); + if + name = type_name + && ext_decl.type_kind = Type_open + && extends file ext_decl + then Hashtbl.replace found (file.uri, stamp) (file, declared))); Hashtbl.to_seq_values found |> List.of_seq - |> List.sort (fun (a : Constructor.t) (b : Constructor.t) -> - compare - (a.cname.loc.loc_start.pos_fname, a.cname.loc.loc_start.pos_cnum) - (b.cname.loc.loc_start.pos_fname, b.cname.loc.loc_start.pos_cnum)) - |> List.map (fun (c : Constructor.t) -> - {c with type_decl = (type_name, decl)}) + |> List.sort + (fun + ((_, a) : File.t * Constructor.t Declared.t) + ((_, b) : File.t * Constructor.t Declared.t) + -> + compare + ( a.item.cname.loc.loc_start.pos_fname, + a.item.cname.loc.loc_start.pos_cnum ) + ( b.item.cname.loc.loc_start.pos_fname, + b.item.cname.loc.loc_start.pos_cnum )) + |> List.map (fun ((file : File.t), (declared : Constructor.t Declared.t)) -> + let path = + Module_path.to_path declared.module_path declared.item.cname.txt + in + let path = + if file.uri = current_file.uri then path else file.module_name :: path + in + { + declared.item with + cname = {declared.item.cname with txt = String.concat "." path}; + type_decl = (type_name, decl); + }) let rec extract_type ?(print_opening_debug = true) ?(type_arg_context : type_arg_context option) @@ -466,14 +517,15 @@ let rec extract_type ?(print_opening_debug = true) variant_decl = decl; }, type_arg_context ) - | Some (env_from_item, {name; item = {decl; kind = Type.Open}}) + | Some (env_from_item, {name; stamp; item = {decl; kind = Type.Open}}) when decl.type_manifest = None -> ( let type_arg_context = maybe_set_type_arg_ctx ~type_params:decl.type_params ~type_args env in match - extension_constructors ~files:[env_from_item.file] ~type_name:name.txt - ~decl + extension_constructors ~state ~package ~current_file:env.file + ~type_file:env_from_item.file ~type_stamp:(Some stamp) + ~type_name:name.txt ~decl with | [] when name.txt = "t" -> Some (TtypeT {env = env_from_item; path}, type_arg_context) @@ -646,8 +698,9 @@ let extract_type_from_resolved_type (typ : Type.t) ~env ~full ~state = {env; constructors; variant_name = typ.name; variant_decl = typ.decl}) | Open when typ.decl.type_manifest = None -> ( match - extension_constructors ~files:[env.file; full.file] ~type_name:typ.name - ~decl:typ.decl + extension_constructors ~state ~package:full.package + ~current_file:full.file ~type_file:env.file + ~type_stamp:(type_stamp env.file typ) ~type_name:typ.name ~decl:typ.decl with | [] -> None | constructors -> diff --git a/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res b/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res index 6d79c41e53..6ac8fb6f63 100644 --- a/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res +++ b/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res @@ -9,9 +9,32 @@ module Sub = { // ^com let value: attr = Str("s") +// ^hov // switch value { | S // ^com // switch valu // ^com + +// Open types of the same name are told apart +module A = { + type t = .. + type t += FromA +} +module B = { + type t = .. + type t += FromB +} +type A.t += MoreA + +let a: A.t = A.FromA + +// switch a { | M +// ^com + +// switch a { | F +// ^com + +// switch value { | Fl +// ^com diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt index c143e2f36d..d8014aa829 100644 --- a/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt +++ b/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt @@ -23,9 +23,17 @@ Path attr { "detail": "module Null", "kind": 9, "label": "Null", "tags": [] } ] -Complete src/CompletionExtensibleVariant.res 12:21 -pos_cursor:[12:21] pos_no_white:[12:20] Found pattern:[12:20->12:21] -Ppat_construct S:[12:20->12:21] +Hover src/CompletionExtensibleVariant.res 10:19 +{ + "contents": { + "kind": "markdown", + "value": "```rescript\nattr\n```\n\n---\n\n```\n \n```\n```rescript\ntype attr = ..\n```\nGo to: [Type definition](CompletionExtensibleVariant.res#L1,1)\n" + } +} + +Complete src/CompletionExtensibleVariant.res 13:21 +pos_cursor:[13:21] pos_no_white:[13:20] Found pattern:[13:20->13:21] +Ppat_construct S:[13:20->13:21] Completable: Cpattern Value[value]=S Package opens Stdlib.place holder Pervasives.JsxModules.place holder Resolved opens 1 Stdlib @@ -43,10 +51,22 @@ Path value "kind": 4, "label": "Str(_)", "tags": [] + }, + { + "detail": "Sub.Flag", + "documentation": { + "kind": "markdown", + "value": "```rescript\nSub.Flag\n```\n\n```rescript\ntype attr = ..\n```" + }, + "insertText": "Sub.Flag", + "insertTextFormat": 2, + "kind": 4, + "label": "Sub.Flag", + "tags": [] } ] -Complete src/CompletionExtensibleVariant.res 15:14 +Complete src/CompletionExtensibleVariant.res 16:14 XXX Not found! Completable: CexhaustiveSwitch Value[valu] Package opens Stdlib.place holder Pervasives.JsxModules.place holder @@ -58,7 +78,7 @@ Path valu { "detail": "insert exhaustive switch for value", "filterText": "value", - "insertText": "value {\n | Str(_) => ${1:%todo}\n | Num(_) => ${2:%todo}\n | Flag => ${3:%todo}\n | _ => ${4:%todo}\n }", + "insertText": "value {\n | Str(_) => ${1:%todo}\n | Num(_) => ${2:%todo}\n | Sub.Flag => ${3:%todo}\n | _ => ${4:%todo}\n }", "insertTextFormat": 2, "kind": 15, "label": "value (exhaustive switch)", @@ -66,3 +86,72 @@ Path valu } ] +Complete src/CompletionExtensibleVariant.res 32:17 +pos_cursor:[32:17] pos_no_white:[32:16] Found pattern:[32:16->32:17] +Ppat_construct M:[32:16->32:17] +Completable: Cpattern Value[a]=M +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[a] +Path a +[ + { + "detail": "MoreA", + "documentation": { + "kind": "markdown", + "value": "```rescript\nMoreA\n```\n\n```rescript\ntype t = ..\n```" + }, + "insertText": "MoreA", + "insertTextFormat": 2, + "kind": 4, + "label": "MoreA", + "tags": [] + } +] + +Complete src/CompletionExtensibleVariant.res 35:17 +pos_cursor:[35:17] pos_no_white:[35:16] Found pattern:[35:16->35:17] +Ppat_construct F:[35:16->35:17] +Completable: Cpattern Value[a]=F +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[a] +Path a +[ + { + "detail": "A.FromA", + "documentation": { + "kind": "markdown", + "value": "```rescript\nA.FromA\n```\n\n```rescript\ntype t = ..\n```" + }, + "insertText": "A.FromA", + "insertTextFormat": 2, + "kind": 4, + "label": "A.FromA", + "tags": [] + } +] + +Complete src/CompletionExtensibleVariant.res 38:22 +pos_cursor:[38:22] pos_no_white:[38:21] Found pattern:[38:20->38:22] +Ppat_construct Fl:[38:20->38:22] +Completable: Cpattern Value[value]=Fl +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[value] +Path value +[ + { + "detail": "Sub.Flag", + "documentation": { + "kind": "markdown", + "value": "```rescript\nSub.Flag\n```\n\n```rescript\ntype attr = ..\n```" + }, + "insertText": "Sub.Flag", + "insertTextFormat": 2, + "kind": 4, + "label": "Sub.Flag", + "tags": [] + } +] + From f2fd4252e167723c4fc0ec122e4d567bc947cda5 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Tue, 27 Oct 2026 20:29:15 +0000 Subject: [PATCH 03/10] Name extension constructors relative to where completion runs Codex review on #9085: extension constructors were named relative to the root of their file and to the file of the completed value. Inside the declaring module that inserted `Sub.Flag`, which is not in scope there, and for a value imported from another file the constructors were not qualified with its module, while extensions in the edited file were missed. A completion request now records the edited file and the module path at the cursor; constructors of that file drop the module path shared with the cursor, constructors of other files get their module's name, and extensions are collected from the edited file. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3 Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo Signed-off-by: Christoph Knittel --- analysis/src/completion_back_end.ml | 36 +++++++++- analysis/src/type_utils.ml | 26 +++++++- .../tests/src/CompletionExtensibleVariant.res | 3 + .../src/CompletionExtensibleVariantUse.res | 6 ++ .../CompletionExtensibleVariant.res.txt | 66 ++++++++++++++----- .../CompletionExtensibleVariantUse.res.txt | 25 +++++++ 6 files changed, 143 insertions(+), 19 deletions(-) create mode 100644 tests/analysis_tests/tests/src/CompletionExtensibleVariantUse.res create mode 100644 tests/analysis_tests/tests/src/expected/CompletionExtensibleVariantUse.res.txt diff --git a/analysis/src/completion_back_end.ml b/analysis/src/completion_back_end.ml index d2de91a4ba..be92c9bdcf 100644 --- a/analysis/src/completion_back_end.ml +++ b/analysis/src/completion_back_end.ml @@ -2105,8 +2105,8 @@ let rec complete_typed_value ?(type_arg_context : type_arg_context option) module String_set = Set.Make (String) -let rec process_completable ~state ~debug ~full ~scope ~env ~pos ~for_hover - completable = +let rec process_completable_in_scope ~state ~debug ~full ~scope ~env ~pos + ~for_hover completable = if debug then Printf.printf "Completable: %s\n" (Completable.to_string completable); let package = full.package in @@ -2412,7 +2412,8 @@ let rec process_completable ~state ~debug ~full ~scope ~env ~pos ~for_hover match (fallback, items) with | Some fallback, (None | Some []) -> fallback - |> process_completable ~state ~debug ~full ~scope ~env ~pos ~for_hover + |> process_completable_in_scope ~state ~debug ~full ~scope ~env ~pos + ~for_hover | _, Some items -> items | None, None -> [] in @@ -2647,3 +2648,32 @@ let rec process_completable ~state ~debug ~full ~scope ~env ~pos ~for_hover ~env ~insert_text:"todo(\"${0:TODO}\")"; ] else [] + +(* The names of the modules around [pos] in [file], outermost first. *) +let module_path_at ~(full : full) ~pos = + let rec in_items (items : Module.item list) = + match + items + |> List.find_opt (fun (item : Module.item) -> + Loc.has_pos ~pos item.loc + && + match item.kind with + | Module _ -> true + | _ -> false) + with + | Some {name; kind = Module {type_}} -> name :: in_module type_ + | _ -> [] + and in_module (m : Module.t) = + match m with + | Structure {items} -> in_items items + | Constraint (implementation, _) -> in_module implementation + | Ident _ -> [] + in + in_items full.file.structure.items + +let process_completable ~state ~debug ~full ~scope ~env ~pos ~for_hover + completable = + Type_utils.with_completion_scope ~file:full.file + ~module_path:(module_path_at ~full ~pos) (fun () -> + process_completable_in_scope ~state ~debug ~full ~scope ~env ~pos + ~for_hover completable) diff --git a/analysis/src/type_utils.ml b/analysis/src/type_utils.ml index 7e0efeab46..76961cef2c 100644 --- a/analysis/src/type_utils.ml +++ b/analysis/src/type_utils.ml @@ -361,6 +361,17 @@ let extract_function_type2 ?type_arg_context ~state ~env ~package typ = in loop ?type_arg_context ~env [] typ +(* The file being edited and the module path at the cursor, while a + completion request runs. Extension constructors are named relative to + it: unlike ordinary constructors they can't be found from the expected + type, so they have to be written so that they are in scope there. *) +let completion_scope : (File.t * string list) option ref = ref None + +let with_completion_scope ~file ~module_path f = + let previous = !completion_scope in + completion_scope := Some (file, module_path); + Fun.protect ~finally:(fun () -> completion_scope := previous) f + (* The stamp under which [typ] is declared in [file]. *) let type_stamp (file : File.t) (typ : Type.t) = let found = ref None in @@ -377,6 +388,11 @@ let type_stamp (file : File.t) (typ : Type.t) = constructor can't be found from the expected type alone. *) let extension_constructors ~state ~package ~(current_file : File.t) ~(type_file : File.t) ~type_stamp ~type_name ~decl = + let current_file, scope_path = + match !completion_scope with + | Some (file, module_path) -> (file, module_path) + | None -> (current_file, []) + in let extends (file : File.t) (ext_decl : Types.type_declaration) = match (type_stamp, ext_decl.type_manifest) with | None, _ -> true @@ -424,8 +440,16 @@ let extension_constructors ~state ~package ~(current_file : File.t) let path = Module_path.to_path declared.module_path declared.item.cname.txt in + (* Inside a module, its own name and those of the modules around it + are not in scope: drop the module path shared with the cursor. *) + let rec relative path scope = + match (path, scope) with + | p :: (_ :: _ as path), s :: scope when p = s -> relative path scope + | _ -> path + in let path = - if file.uri = current_file.uri then path else file.module_name :: path + if file.uri = current_file.uri then relative path scope_path + else file.module_name :: path in { declared.item with diff --git a/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res b/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res index 6ac8fb6f63..15b93462a9 100644 --- a/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res +++ b/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res @@ -3,6 +3,9 @@ type attr += Str(string) | Num(int) module Sub = { type attr += Flag + // Inside the declaring module the constructor is not qualified + // let inside: attr = Fl + // ^com } // let a: attr = Nu diff --git a/tests/analysis_tests/tests/src/CompletionExtensibleVariantUse.res b/tests/analysis_tests/tests/src/CompletionExtensibleVariantUse.res new file mode 100644 index 0000000000..e4c99cf221 --- /dev/null +++ b/tests/analysis_tests/tests/src/CompletionExtensibleVariantUse.res @@ -0,0 +1,6 @@ +type CompletionExtensibleVariant.attr += Local + +let imported = CompletionExtensibleVariant.value + +// switch importe +// ^com diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt index d8014aa829..eaeab4e210 100644 --- a/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt +++ b/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt @@ -1,4 +1,40 @@ -Complete src/CompletionExtensibleVariant.res 7:19 +Complete src/CompletionExtensibleVariant.res 6:26 +XXX Not found! +Completable: Cexpression Type[attr]=Fl +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Type[attr] +Path attr +[ + { + "detail": "Flag", + "documentation": { + "kind": "markdown", + "value": "```rescript\nFlag\n```\n\n```rescript\ntype attr = ..\n```" + }, + "insertText": "Flag", + "insertTextFormat": 2, + "kind": 4, + "label": "Flag", + "sortText": "A Flag", + "tags": [] + }, + { + "detail": "module Float32Array", + "kind": 9, + "label": "Float32Array", + "tags": [] + }, + { + "detail": "module Float64Array", + "kind": 9, + "label": "Float64Array", + "tags": [] + }, + { "detail": "module Float", "kind": 9, "label": "Float", "tags": [] } +] + +Complete src/CompletionExtensibleVariant.res 10:19 XXX Not found! Completable: Cexpression Type[attr]=Nu Package opens Stdlib.place holder Pervasives.JsxModules.place holder @@ -23,7 +59,7 @@ Path attr { "detail": "module Null", "kind": 9, "label": "Null", "tags": [] } ] -Hover src/CompletionExtensibleVariant.res 10:19 +Hover src/CompletionExtensibleVariant.res 13:19 { "contents": { "kind": "markdown", @@ -31,9 +67,9 @@ Hover src/CompletionExtensibleVariant.res 10:19 } } -Complete src/CompletionExtensibleVariant.res 13:21 -pos_cursor:[13:21] pos_no_white:[13:20] Found pattern:[13:20->13:21] -Ppat_construct S:[13:20->13:21] +Complete src/CompletionExtensibleVariant.res 16:21 +pos_cursor:[16:21] pos_no_white:[16:20] Found pattern:[16:20->16:21] +Ppat_construct S:[16:20->16:21] Completable: Cpattern Value[value]=S Package opens Stdlib.place holder Pervasives.JsxModules.place holder Resolved opens 1 Stdlib @@ -66,7 +102,7 @@ Path value } ] -Complete src/CompletionExtensibleVariant.res 16:14 +Complete src/CompletionExtensibleVariant.res 19:14 XXX Not found! Completable: CexhaustiveSwitch Value[valu] Package opens Stdlib.place holder Pervasives.JsxModules.place holder @@ -86,9 +122,9 @@ Path valu } ] -Complete src/CompletionExtensibleVariant.res 32:17 -pos_cursor:[32:17] pos_no_white:[32:16] Found pattern:[32:16->32:17] -Ppat_construct M:[32:16->32:17] +Complete src/CompletionExtensibleVariant.res 35:17 +pos_cursor:[35:17] pos_no_white:[35:16] Found pattern:[35:16->35:17] +Ppat_construct M:[35:16->35:17] Completable: Cpattern Value[a]=M Package opens Stdlib.place holder Pervasives.JsxModules.place holder Resolved opens 1 Stdlib @@ -109,9 +145,9 @@ Path a } ] -Complete src/CompletionExtensibleVariant.res 35:17 -pos_cursor:[35:17] pos_no_white:[35:16] Found pattern:[35:16->35:17] -Ppat_construct F:[35:16->35:17] +Complete src/CompletionExtensibleVariant.res 38:17 +pos_cursor:[38:17] pos_no_white:[38:16] Found pattern:[38:16->38:17] +Ppat_construct F:[38:16->38:17] Completable: Cpattern Value[a]=F Package opens Stdlib.place holder Pervasives.JsxModules.place holder Resolved opens 1 Stdlib @@ -132,9 +168,9 @@ Path a } ] -Complete src/CompletionExtensibleVariant.res 38:22 -pos_cursor:[38:22] pos_no_white:[38:21] Found pattern:[38:20->38:22] -Ppat_construct Fl:[38:20->38:22] +Complete src/CompletionExtensibleVariant.res 41:22 +pos_cursor:[41:22] pos_no_white:[41:21] Found pattern:[41:20->41:22] +Ppat_construct Fl:[41:20->41:22] Completable: Cpattern Value[value]=Fl Package opens Stdlib.place holder Pervasives.JsxModules.place holder Resolved opens 1 Stdlib diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariantUse.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariantUse.res.txt new file mode 100644 index 0000000000..2759437427 --- /dev/null +++ b/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariantUse.res.txt @@ -0,0 +1,25 @@ +Complete src/CompletionExtensibleVariantUse.res 4:17 +XXX Not found! +Completable: CexhaustiveSwitch Value[importe] +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[importe] +Path importe +[ + { + "detail": "CompletionExtensibleVariant.attr", + "kind": 12, + "label": "imported", + "tags": [] + }, + { + "detail": "insert exhaustive switch for value", + "filterText": "imported", + "insertText": "imported {\n | CompletionExtensibleVariant.Str(_) => ${1:%todo}\n | CompletionExtensibleVariant.Num(_) => ${2:%todo}\n | CompletionExtensibleVariant.Sub.Flag => ${3:%todo}\n | Local => ${4:%todo}\n | _ => ${5:%todo}\n }", + "insertTextFormat": 2, + "kind": 15, + "label": "imported (exhaustive switch)", + "tags": [] + } +] + From a62bfe579a1c9cd4edf6d26793d3ec84083f1251 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Tue, 27 Oct 2026 21:45:42 +0000 Subject: [PATCH 04/10] Qualify extension constructors of namespaced modules by source path Codex review on #9085: the module name of another file comes from its compiled name, which in a namespaced project is `Base-MyNamespace`, so completion inserted `Base-MyNamespace.Flag`. The name is turned into its source path `MyNamespace.Base` first. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3 Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo Signed-off-by: Christoph Knittel --- analysis/src/type_utils.ml | 2 +- .../src/ExtensibleBase.res | 4 ++++ .../src/ExtensibleUse.res | 4 ++++ .../src/expected/ExtensibleBase.res.txt | 0 .../src/expected/ExtensibleUse.res.txt | 24 +++++++++++++++++++ 5 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 tests/analysis_tests/tests-namespaced-references/src/ExtensibleBase.res create mode 100644 tests/analysis_tests/tests-namespaced-references/src/ExtensibleUse.res create mode 100644 tests/analysis_tests/tests-namespaced-references/src/expected/ExtensibleBase.res.txt create mode 100644 tests/analysis_tests/tests-namespaced-references/src/expected/ExtensibleUse.res.txt diff --git a/analysis/src/type_utils.ml b/analysis/src/type_utils.ml index 76961cef2c..eabc067bc2 100644 --- a/analysis/src/type_utils.ml +++ b/analysis/src/type_utils.ml @@ -449,7 +449,7 @@ let extension_constructors ~state ~package ~(current_file : File.t) in let path = if file.uri = current_file.uri then relative path scope_path - else file.module_name :: path + else Utils.flatten_any_namespace_in_path (file.module_name :: path) in { declared.item with diff --git a/tests/analysis_tests/tests-namespaced-references/src/ExtensibleBase.res b/tests/analysis_tests/tests-namespaced-references/src/ExtensibleBase.res new file mode 100644 index 0000000000..cc1cf75a2b --- /dev/null +++ b/tests/analysis_tests/tests-namespaced-references/src/ExtensibleBase.res @@ -0,0 +1,4 @@ +type attr = .. +type attr += Flag | Size(int) + +let value: attr = Flag diff --git a/tests/analysis_tests/tests-namespaced-references/src/ExtensibleUse.res b/tests/analysis_tests/tests-namespaced-references/src/ExtensibleUse.res new file mode 100644 index 0000000000..3b818c5ed0 --- /dev/null +++ b/tests/analysis_tests/tests-namespaced-references/src/ExtensibleUse.res @@ -0,0 +1,4 @@ +let imported = ExtensibleBase.value + +// switch importe +// ^com diff --git a/tests/analysis_tests/tests-namespaced-references/src/expected/ExtensibleBase.res.txt b/tests/analysis_tests/tests-namespaced-references/src/expected/ExtensibleBase.res.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/analysis_tests/tests-namespaced-references/src/expected/ExtensibleUse.res.txt b/tests/analysis_tests/tests-namespaced-references/src/expected/ExtensibleUse.res.txt new file mode 100644 index 0000000000..a22af674ea --- /dev/null +++ b/tests/analysis_tests/tests-namespaced-references/src/expected/ExtensibleUse.res.txt @@ -0,0 +1,24 @@ +Complete src/ExtensibleUse.res 2:17 +XXX Not found! +Completable: CexhaustiveSwitch Value[importe] +Package opens Stdlib.place holder Pervasives.JsxModules.place holder MyNamespace.place holder +Resolved opens 2 MyNamespace Stdlib +ContextPath Value[importe] +Path importe +[ + { + "detail": "MyNamespace.ExtensibleBase.attr", + "kind": 12, + "label": "imported", + "tags": [] + }, + { + "detail": "insert exhaustive switch for value", + "filterText": "imported", + "insertText": "imported {\n | MyNamespace.ExtensibleBase.Flag => ${1:%todo}\n | MyNamespace.ExtensibleBase.Size(_) => ${2:%todo}\n | _ => ${3:%todo}\n }", + "insertTextFormat": 2, + "kind": 15, + "label": "imported (exhaustive switch)", + "tags": [] + } +] From e6a28afb69eba6e99e3529a7a480f42fbfdc0076 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 28 Oct 2026 07:47:08 +0000 Subject: [PATCH 05/10] Offer extensions in scope at the cursor and from interface modules Codex review on #9085: an extension of the edited file declared after the cursor was offered although it isn't in scope there, and an extension inside a module of an interface (`module Sub: { type attr += Flag }`) was never registered, because only typed-tree extensions were. The completion scope now records the cursor position and skips later extensions of the edited file, and extensions in signatures are registered too. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3 Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo Signed-off-by: Christoph Knittel --- analysis/src/completion_back_end.ml | 2 +- analysis/src/process_cmt.ml | 108 +++++++++--------- analysis/src/type_utils.ml | 20 +++- .../tests/src/CompletionExtensibleIface.res | 8 ++ .../tests/src/CompletionExtensibleIface.resi | 8 ++ .../src/CompletionExtensibleIfaceUse.res | 4 + .../tests/src/CompletionExtensibleVariant.res | 6 + .../CompletionExtensibleIface.res.txt | 0 .../CompletionExtensibleIface.resi.txt | 0 .../CompletionExtensibleIfaceUse.res.txt | 25 ++++ .../CompletionExtensibleVariant.res.txt | 15 +++ .../CompletionExtensibleVariantUse.res.txt | 2 +- 12 files changed, 138 insertions(+), 60 deletions(-) create mode 100644 tests/analysis_tests/tests/src/CompletionExtensibleIface.res create mode 100644 tests/analysis_tests/tests/src/CompletionExtensibleIface.resi create mode 100644 tests/analysis_tests/tests/src/CompletionExtensibleIfaceUse.res create mode 100644 tests/analysis_tests/tests/src/expected/CompletionExtensibleIface.res.txt create mode 100644 tests/analysis_tests/tests/src/expected/CompletionExtensibleIface.resi.txt create mode 100644 tests/analysis_tests/tests/src/expected/CompletionExtensibleIfaceUse.res.txt diff --git a/analysis/src/completion_back_end.ml b/analysis/src/completion_back_end.ml index be92c9bdcf..29ac4184cb 100644 --- a/analysis/src/completion_back_end.ml +++ b/analysis/src/completion_back_end.ml @@ -2674,6 +2674,6 @@ let module_path_at ~(full : full) ~pos = let process_completable ~state ~debug ~full ~scope ~env ~pos ~for_hover completable = Type_utils.with_completion_scope ~file:full.file - ~module_path:(module_path_at ~full ~pos) (fun () -> + ~module_path:(module_path_at ~full ~pos) ~pos (fun () -> process_completable_in_scope ~state ~debug ~full ~scope ~env ~pos ~for_hover completable) diff --git a/analysis/src/process_cmt.ml b/analysis/src/process_cmt.ml index 270b4b2b8b..a7bf591ca3 100644 --- a/analysis/src/process_cmt.ml +++ b/analysis/src/process_cmt.ml @@ -35,6 +35,54 @@ let map_record_field {Types.ld_id; ld_type; ld_attributes; ld_optional} = deprecated = Process_attributes.find_deprecated_attribute ld_attributes; } +(* The constructors of [type t += ...] are registered as constructors of an + open type named [t], so that completion can offer them for values of + type [t]. They are not part of any type declaration, hence the stand-in + declaration that carries the kind and, as its manifest, the extended + type, which completion compares with the type it completes. *) +let add_extension_constructor ~(env : Env.t) ~id ~name + (ext : Types.extension_constructor) = + let type_decl : Types.type_declaration = + { + type_params = []; + type_arity = 0; + type_kind = Type_open; + type_private = Public; + type_manifest = Some (Ctype.newconstr ext.ext_type_path []); + type_variance = []; + type_newtype_level = None; + type_loc = Location.none; + type_attributes = []; + type_immediate = false; + type_representation = Boxed; + type_shape = Unstated; + type_inlined_types = []; + } + in + let stamp = Ident.binding_time id in + let item = + { + Constructor.stamp; + cname = name; + args = + (match ext.ext_args with + | Cstr_tuple args -> + Args (args |> List.map (fun t -> (t, Location.none))) + | Cstr_record fields -> + InlineRecord (fields |> List.map map_record_field)); + res = ext.ext_ret_type; + type_decl = (Path.last ext.ext_type_path, type_decl); + docstring = attrs_to_docstring ext.ext_attributes; + deprecated = + Process_attributes.find_deprecated_attribute ext.ext_attributes; + } + in + let declared = + Process_attributes.new_declared ~item ~extent:ext.ext_loc ~name ~stamp + ~module_path:env.module_path true ext.ext_attributes + in + Stamps.add_constructor env.stamps stamp declared + let rec for_type_signature_item ~(env : Shared_types.Env.t) ~(exported : Exported.t) (item : Types.signature_item) = match item with @@ -165,6 +213,11 @@ let rec for_type_signature_item ~(env : Shared_types.Env.t) loc = declared.extent_loc; }; ] + | Sig_typext (id, ext, _) -> + add_extension_constructor ~env ~id + ~name:(Location.mkloc (Ident.name id) ext.ext_loc) + ext; + [] | _ -> [] and for_type_signature ~name ~env signature = @@ -328,60 +381,11 @@ let for_type_declaration ~env ~(exported : Exported.t) loc = declared.extent_loc; } -(* The constructors of [type t += ...] are registered as constructors of an - open type named [t], so that completion can offer them for values of - type [t]. They are not part of any type declaration, hence the stand-in - declaration that carries the kind and, as its manifest, the extended - type, which completion compares with the type it completes. *) -let add_extension_constructors ~(env : Env.t) - {Typedtree.tyext_path; tyext_constructors} = - let type_name = Path.last tyext_path in - let type_decl : Types.type_declaration = - { - type_params = []; - type_arity = 0; - type_kind = Type_open; - type_private = Public; - type_manifest = Some (Ctype.newconstr tyext_path []); - type_variance = []; - type_newtype_level = None; - type_loc = Location.none; - type_attributes = []; - type_immediate = false; - type_representation = Boxed; - type_shape = Unstated; - type_inlined_types = []; - } - in +let add_extension_constructors ~(env : Env.t) {Typedtree.tyext_constructors} = tyext_constructors |> List.iter - (fun - ({ext_id; ext_name; ext_type; ext_loc; ext_attributes} : - Typedtree.extension_constructor) - -> - let stamp = Ident.binding_time ext_id in - let item = - { - Constructor.stamp; - cname = ext_name; - args = - (match ext_type.ext_args with - | Cstr_tuple args -> - Args (args |> List.map (fun t -> (t, Location.none))) - | Cstr_record fields -> - InlineRecord (fields |> List.map map_record_field)); - res = ext_type.ext_ret_type; - type_decl = (type_name, type_decl); - docstring = attrs_to_docstring ext_attributes; - deprecated = - Process_attributes.find_deprecated_attribute ext_attributes; - } - in - let declared = - Process_attributes.new_declared ~item ~extent:ext_loc ~name:ext_name - ~stamp ~module_path:env.module_path true ext_attributes - in - Stamps.add_constructor env.stamps stamp declared) + (fun ({ext_id; ext_name; ext_type} : Typedtree.extension_constructor) -> + add_extension_constructor ~env ~id:ext_id ~name:ext_name ext_type) let rec for_signature_item ~env ~(exported : Exported.t) (item : Typedtree.signature_item) = diff --git a/analysis/src/type_utils.ml b/analysis/src/type_utils.ml index eabc067bc2..a6fc845662 100644 --- a/analysis/src/type_utils.ml +++ b/analysis/src/type_utils.ml @@ -365,11 +365,11 @@ let extract_function_type2 ?type_arg_context ~state ~env ~package typ = completion request runs. Extension constructors are named relative to it: unlike ordinary constructors they can't be found from the expected type, so they have to be written so that they are in scope there. *) -let completion_scope : (File.t * string list) option ref = ref None +let completion_scope : (File.t * string list * Pos.t) option ref = ref None -let with_completion_scope ~file ~module_path f = +let with_completion_scope ~file ~module_path ~pos f = let previous = !completion_scope in - completion_scope := Some (file, module_path); + completion_scope := Some (file, module_path, pos); Fun.protect ~finally:(fun () -> completion_scope := previous) f (* The stamp under which [typ] is declared in [file]. *) @@ -388,10 +388,17 @@ let type_stamp (file : File.t) (typ : Type.t) = constructor can't be found from the expected type alone. *) let extension_constructors ~state ~package ~(current_file : File.t) ~(type_file : File.t) ~type_stamp ~type_name ~decl = - let current_file, scope_path = + let current_file, scope_path, cursor = match !completion_scope with - | Some (file, module_path) -> (file, module_path) - | None -> (current_file, []) + | Some (file, module_path, pos) -> (file, module_path, Some pos) + | None -> (current_file, [], None) + in + (* An extension of the edited file is in scope from its declaration on. *) + let declared_before_cursor (file : File.t) (declared : _ Declared.t) = + match cursor with + | Some pos when file.uri = current_file.uri -> + Loc.start declared.extent_loc <= pos + | _ -> true in let extends (file : File.t) (ext_decl : Types.type_declaration) = match (type_stamp, ext_decl.type_manifest) with @@ -422,6 +429,7 @@ let extension_constructors ~state ~package ~(current_file : File.t) if name = type_name && ext_decl.type_kind = Type_open + && declared_before_cursor file declared && extends file ext_decl then Hashtbl.replace found (file.uri, stamp) (file, declared))); Hashtbl.to_seq_values found diff --git a/tests/analysis_tests/tests/src/CompletionExtensibleIface.res b/tests/analysis_tests/tests/src/CompletionExtensibleIface.res new file mode 100644 index 0000000000..a4e7404e62 --- /dev/null +++ b/tests/analysis_tests/tests/src/CompletionExtensibleIface.res @@ -0,0 +1,8 @@ +type attr = .. +type attr += Top + +module Sub = { + type attr += Flag +} + +let value: attr = Top diff --git a/tests/analysis_tests/tests/src/CompletionExtensibleIface.resi b/tests/analysis_tests/tests/src/CompletionExtensibleIface.resi new file mode 100644 index 0000000000..5c159033e2 --- /dev/null +++ b/tests/analysis_tests/tests/src/CompletionExtensibleIface.resi @@ -0,0 +1,8 @@ +type attr = .. +type attr += Top + +module Sub: { + type attr += Flag +} + +let value: attr diff --git a/tests/analysis_tests/tests/src/CompletionExtensibleIfaceUse.res b/tests/analysis_tests/tests/src/CompletionExtensibleIfaceUse.res new file mode 100644 index 0000000000..32f64d6f92 --- /dev/null +++ b/tests/analysis_tests/tests/src/CompletionExtensibleIfaceUse.res @@ -0,0 +1,4 @@ +let imported = CompletionExtensibleIface.value + +// switch importe +// ^com diff --git a/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res b/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res index 15b93462a9..2d1a936ada 100644 --- a/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res +++ b/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res @@ -41,3 +41,9 @@ let a: A.t = A.FromA // switch value { | Fl // ^com + +// An extension declared later is not in scope yet +// switch value { | La +// ^com + +type attr += Later diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensibleIface.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensibleIface.res.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensibleIface.resi.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensibleIface.resi.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensibleIfaceUse.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensibleIfaceUse.res.txt new file mode 100644 index 0000000000..e6be459049 --- /dev/null +++ b/tests/analysis_tests/tests/src/expected/CompletionExtensibleIfaceUse.res.txt @@ -0,0 +1,25 @@ +Complete src/CompletionExtensibleIfaceUse.res 2:17 +XXX Not found! +Completable: CexhaustiveSwitch Value[importe] +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[importe] +Path importe +[ + { + "detail": "CompletionExtensibleIface.attr", + "kind": 12, + "label": "imported", + "tags": [] + }, + { + "detail": "insert exhaustive switch for value", + "filterText": "imported", + "insertText": "imported {\n | CompletionExtensibleIface.Top => ${1:%todo}\n | CompletionExtensibleIface.Sub.Flag => ${2:%todo}\n | _ => ${3:%todo}\n }", + "insertTextFormat": 2, + "kind": 15, + "label": "imported (exhaustive switch)", + "tags": [] + } +] + diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt index eaeab4e210..4ba7727364 100644 --- a/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt +++ b/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt @@ -191,3 +191,18 @@ Path value } ] +Complete src/CompletionExtensibleVariant.res 45:22 +pos_cursor:[45:22] pos_no_white:[45:21] Found pattern:[45:20->45:22] +Ppat_construct La:[45:20->45:22] +Completable: Cpattern Value[value]=La +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[value] +Path value +Completable: Cpath Value[La] +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[La] +Path La +[ { "detail": "module Lazy", "kind": 9, "label": "Lazy", "tags": [] } ] + diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariantUse.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariantUse.res.txt index 2759437427..b3ea8edd0d 100644 --- a/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariantUse.res.txt +++ b/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariantUse.res.txt @@ -15,7 +15,7 @@ Path importe { "detail": "insert exhaustive switch for value", "filterText": "imported", - "insertText": "imported {\n | CompletionExtensibleVariant.Str(_) => ${1:%todo}\n | CompletionExtensibleVariant.Num(_) => ${2:%todo}\n | CompletionExtensibleVariant.Sub.Flag => ${3:%todo}\n | Local => ${4:%todo}\n | _ => ${5:%todo}\n }", + "insertText": "imported {\n | CompletionExtensibleVariant.Str(_) => ${1:%todo}\n | CompletionExtensibleVariant.Num(_) => ${2:%todo}\n | CompletionExtensibleVariant.Sub.Flag => ${3:%todo}\n | CompletionExtensibleVariant.Later => ${4:%todo}\n | Local => ${5:%todo}\n | _ => ${6:%todo}\n }", "insertTextFormat": 2, "kind": 15, "label": "imported (exhaustive switch)", From 52ee62da8df56b532b7a199ff214536d4d80d759 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Thu, 29 Oct 2026 06:34:31 +0000 Subject: [PATCH 06/10] Qualify extensions of nested interface modules and complete their payloads Codex review on #9085: an extension two modules deep in an interface got only the outer module in its path, because the signature walk didn't extend the module path when entering a module, and completion inside the payload of a qualified constructor such as `Sub.Toggle(...)` found no constructor, because the payload position keeps only `Toggle`. Signature modules now extend the path, and payload lookup also matches the last part of a qualified constructor name. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Kbf3gLMawVgtmiKy3TMBc3 Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo Signed-off-by: Christoph Knittel --- analysis/src/process_cmt.ml | 2 +- analysis/src/type_utils.ml | 15 ++++++- .../tests/src/CompletionExtensibleIface.res | 6 +++ .../tests/src/CompletionExtensibleIface.resi | 6 +++ .../tests/src/CompletionExtensibleVariant.res | 6 ++- .../CompletionExtensibleIfaceUse.res.txt | 2 +- .../CompletionExtensibleVariant.res.txt | 41 ++++++++++++++++++- .../CompletionExtensibleVariantUse.res.txt | 2 +- 8 files changed, 73 insertions(+), 7 deletions(-) diff --git a/analysis/src/process_cmt.ml b/analysis/src/process_cmt.ml index a7bf591ca3..ebbd4b6a01 100644 --- a/analysis/src/process_cmt.ml +++ b/analysis/src/process_cmt.ml @@ -196,7 +196,7 @@ let rec for_type_signature_item ~(env : Shared_types.Env.t) let name = Ident.name ident in let declared = add_declared ~extent:md_loc - ~item:(for_type_module ~name ~env md_type) + ~item:(for_type_module ~name ~env:(env |> Env.add_module ~name) md_type) ~name:(Location.mkloc name md_loc) ~stamp:(Ident.binding_time ident) ~env md_attributes (Exported.add exported Exported.Module) diff --git a/analysis/src/type_utils.ml b/analysis/src/type_utils.ml index a6fc845662..24bc7bee3b 100644 --- a/analysis/src/type_utils.ml +++ b/analysis/src/type_utils.ml @@ -372,6 +372,17 @@ let with_completion_scope ~file ~module_path ~pos f = completion_scope := Some (file, module_path, pos); Fun.protect ~finally:(fun () -> completion_scope := previous) f +(* Whether [c] is the constructor written [name] in a payload position, where + only the last part of a path such as [Sub.Flag] is kept. Extension + constructors are named by their module path, ordinary ones are not. *) +let constructor_has_name (c : Constructor.t) name = + c.cname.txt = name + || + match String.rindex_opt c.cname.txt '.' with + | Some i -> + String.sub c.cname.txt (i + 1) (String.length c.cname.txt - i - 1) = name + | None -> false + (* The stamp under which [typ] is declared in [file]. *) let type_stamp (file : File.t) (typ : Type.t) = let found = ref None in @@ -909,7 +920,7 @@ let rec resolve_nested ?type_arg_context ~env ~full ~state ~nested ?ctx match constructors |> List.find_opt (fun (c : Constructor.t) -> - c.cname.txt = constructor_name) + constructor_has_name c constructor_name) with | Some {args = Args args} -> ( if Debug.verbose () then @@ -1045,7 +1056,7 @@ let rec resolve_nested_pattern_path (typ : inner_type) ~env ~full ~state ~nested match constructors |> List.find_opt (fun (constructor : Constructor.t) -> - constructor.cname.txt = constructor_name) + constructor_has_name constructor constructor_name) with | Some {args = Args args} -> ( let item_num, nested = diff --git a/tests/analysis_tests/tests/src/CompletionExtensibleIface.res b/tests/analysis_tests/tests/src/CompletionExtensibleIface.res index a4e7404e62..e472b52a67 100644 --- a/tests/analysis_tests/tests/src/CompletionExtensibleIface.res +++ b/tests/analysis_tests/tests/src/CompletionExtensibleIface.res @@ -5,4 +5,10 @@ module Sub = { type attr += Flag } +module Outer = { + module Inner = { + type attr += Deep + } +} + let value: attr = Top diff --git a/tests/analysis_tests/tests/src/CompletionExtensibleIface.resi b/tests/analysis_tests/tests/src/CompletionExtensibleIface.resi index 5c159033e2..bb96a61c07 100644 --- a/tests/analysis_tests/tests/src/CompletionExtensibleIface.resi +++ b/tests/analysis_tests/tests/src/CompletionExtensibleIface.resi @@ -5,4 +5,10 @@ module Sub: { type attr += Flag } +module Outer: { + module Inner: { + type attr += Deep + } +} + let value: attr diff --git a/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res b/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res index 2d1a936ada..c031639d1b 100644 --- a/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res +++ b/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res @@ -2,7 +2,7 @@ type attr = .. type attr += Str(string) | Num(int) module Sub = { - type attr += Flag + type attr += Flag | Toggle(bool) // Inside the declaring module the constructor is not qualified // let inside: attr = Fl // ^com @@ -47,3 +47,7 @@ let a: A.t = A.FromA // ^com type attr += Later + +// Payload of a qualified extension constructor +// let toggled: attr = Sub.Toggle(t +// ^com diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensibleIfaceUse.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensibleIfaceUse.res.txt index e6be459049..62f4690936 100644 --- a/tests/analysis_tests/tests/src/expected/CompletionExtensibleIfaceUse.res.txt +++ b/tests/analysis_tests/tests/src/expected/CompletionExtensibleIfaceUse.res.txt @@ -15,7 +15,7 @@ Path importe { "detail": "insert exhaustive switch for value", "filterText": "imported", - "insertText": "imported {\n | CompletionExtensibleIface.Top => ${1:%todo}\n | CompletionExtensibleIface.Sub.Flag => ${2:%todo}\n | _ => ${3:%todo}\n }", + "insertText": "imported {\n | CompletionExtensibleIface.Top => ${1:%todo}\n | CompletionExtensibleIface.Sub.Flag => ${2:%todo}\n | CompletionExtensibleIface.Outer.Inner.Deep => ${3:%todo}\n | _ => ${4:%todo}\n }", "insertTextFormat": 2, "kind": 15, "label": "imported (exhaustive switch)", diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt index 4ba7727364..f3e9907dcc 100644 --- a/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt +++ b/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt @@ -99,6 +99,18 @@ Path value "kind": 4, "label": "Sub.Flag", "tags": [] + }, + { + "detail": "Sub.Toggle(bool)", + "documentation": { + "kind": "markdown", + "value": "```rescript\nSub.Toggle(bool)\n```\n\n```rescript\ntype attr = ..\n```" + }, + "insertText": "Sub.Toggle(${1:_})", + "insertTextFormat": 2, + "kind": 4, + "label": "Sub.Toggle(_)", + "tags": [] } ] @@ -114,7 +126,7 @@ Path valu { "detail": "insert exhaustive switch for value", "filterText": "value", - "insertText": "value {\n | Str(_) => ${1:%todo}\n | Num(_) => ${2:%todo}\n | Sub.Flag => ${3:%todo}\n | _ => ${4:%todo}\n }", + "insertText": "value {\n | Str(_) => ${1:%todo}\n | Num(_) => ${2:%todo}\n | Sub.Flag => ${3:%todo}\n | Sub.Toggle(_) => ${4:%todo}\n | _ => ${5:%todo}\n }", "insertTextFormat": 2, "kind": 15, "label": "value (exhaustive switch)", @@ -206,3 +218,30 @@ ContextPath Value[La] Path La [ { "detail": "module Lazy", "kind": 9, "label": "Lazy", "tags": [] } ] +Complete src/CompletionExtensibleVariant.res 51:35 +XXX Not found! +Completable: Cexpression Type[attr]=t->variantPayload::Toggle($0) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Type[attr] +Path attr +[ + { + "detail": "bool", + "kind": 4, + "label": "true", + "sortText": "A true", + "tags": [] + }, + { + "detail": "'a => Type.t", + "documentation": { + "kind": "markdown", + "value": "\n`typeof(value)` exposes JavaScript's `typeof` operator and returns a `Type.t` enum.\nIt helps you inspect values that come from JavaScript APIs.\n\n## Examples\n\n```rescript\ntypeof(1) == #number\ntypeof(\"a\") == #string\n```\n" + }, + "kind": 12, + "label": "typeof", + "tags": [] + } +] + diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariantUse.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariantUse.res.txt index b3ea8edd0d..9fde725d06 100644 --- a/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariantUse.res.txt +++ b/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariantUse.res.txt @@ -15,7 +15,7 @@ Path importe { "detail": "insert exhaustive switch for value", "filterText": "imported", - "insertText": "imported {\n | CompletionExtensibleVariant.Str(_) => ${1:%todo}\n | CompletionExtensibleVariant.Num(_) => ${2:%todo}\n | CompletionExtensibleVariant.Sub.Flag => ${3:%todo}\n | CompletionExtensibleVariant.Later => ${4:%todo}\n | Local => ${5:%todo}\n | _ => ${6:%todo}\n }", + "insertText": "imported {\n | CompletionExtensibleVariant.Str(_) => ${1:%todo}\n | CompletionExtensibleVariant.Num(_) => ${2:%todo}\n | CompletionExtensibleVariant.Sub.Flag => ${3:%todo}\n | CompletionExtensibleVariant.Sub.Toggle(_) => ${4:%todo}\n | CompletionExtensibleVariant.Later => ${5:%todo}\n | Local => ${6:%todo}\n | _ => ${7:%todo}\n }", "insertTextFormat": 2, "kind": 15, "label": "imported (exhaustive switch)", From b03db7b1b5709027ac8c5547bc6ca6a483653ad6 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Thu, 29 Oct 2026 10:10:22 +0000 Subject: [PATCH 07/10] Type extension payloads in their own file and match qualified constructors Codex review on #9085: a payload type declared next to the extension, as in `type Base.attr += Color(color)` with a local `color`, was resolved in the open type's file, so payload completion lost it; and of two same-named extensions, `B.Value(...)` completed the payload of the first one, `A.Value`, because the payload position kept only `Value`. Each extension constructor now carries the environment of its file, and the payload position keeps the written path: the constructor sharing the longest suffix with it is meant, ties going to the most recent open, then to the last declared one. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Kbf3gGkPma9LAZimuQjnrP Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo Signed-off-by: Christoph Knittel --- analysis/src/completion_back_end.ml | 7 +- analysis/src/completion_expressions.ml | 3 + analysis/src/completion_front_end.ml | 1 + analysis/src/completion_patterns.ml | 3 + analysis/src/shared_types.ml | 5 + analysis/src/type_utils.ml | 166 +++- .../tests/src/CompletionExtensiblePayload.res | 102 ++ .../src/CompletionExtensiblePayloadBase.res | 3 + .../src/CompletionExtensiblePayloadIface.res | 5 + .../src/CompletionExtensiblePayloadIface.resi | 5 + .../CompletionExtensiblePayloadIfaceUse.res | 3 + .../src/CompletionExtensiblePayloadThird.res | 1 + .../CompletionExtensiblePayload.res.txt | 891 ++++++++++++++++++ .../CompletionExtensiblePayloadBase.res.txt | 0 .../CompletionExtensiblePayloadIface.res.txt | 0 .../CompletionExtensiblePayloadIface.resi.txt | 0 ...ompletionExtensiblePayloadIfaceUse.res.txt | 34 + .../CompletionExtensiblePayloadThird.res.txt | 0 18 files changed, 1182 insertions(+), 47 deletions(-) create mode 100644 tests/analysis_tests/tests/src/CompletionExtensiblePayload.res create mode 100644 tests/analysis_tests/tests/src/CompletionExtensiblePayloadBase.res create mode 100644 tests/analysis_tests/tests/src/CompletionExtensiblePayloadIface.res create mode 100644 tests/analysis_tests/tests/src/CompletionExtensiblePayloadIface.resi create mode 100644 tests/analysis_tests/tests/src/CompletionExtensiblePayloadIfaceUse.res create mode 100644 tests/analysis_tests/tests/src/CompletionExtensiblePayloadThird.res create mode 100644 tests/analysis_tests/tests/src/expected/CompletionExtensiblePayload.res.txt create mode 100644 tests/analysis_tests/tests/src/expected/CompletionExtensiblePayloadBase.res.txt create mode 100644 tests/analysis_tests/tests/src/expected/CompletionExtensiblePayloadIface.res.txt create mode 100644 tests/analysis_tests/tests/src/expected/CompletionExtensiblePayloadIface.resi.txt create mode 100644 tests/analysis_tests/tests/src/expected/CompletionExtensiblePayloadIfaceUse.res.txt create mode 100644 tests/analysis_tests/tests/src/expected/CompletionExtensiblePayloadThird.res.txt diff --git a/analysis/src/completion_back_end.ml b/analysis/src/completion_back_end.ml index 29ac4184cb..85c3793d22 100644 --- a/analysis/src/completion_back_end.ml +++ b/analysis/src/completion_back_end.ml @@ -2673,7 +2673,12 @@ let module_path_at ~(full : full) ~pos = let process_completable ~state ~debug ~full ~scope ~env ~pos ~for_hover completable = + (* A raw open ends in a placeholder for the name it qualifies. *) + let opens = + Scope.get_raw_opens scope + |> List.map (List.filter (fun name -> name <> "place holder")) + in Type_utils.with_completion_scope ~file:full.file - ~module_path:(module_path_at ~full ~pos) ~pos (fun () -> + ~module_path:(module_path_at ~full ~pos) ~pos ~opens (fun () -> process_completable_in_scope ~state ~debug ~full ~scope ~env ~pos ~for_hover completable) diff --git a/analysis/src/completion_expressions.ml b/analysis/src/completion_expressions.ml index 5a69c4bc84..ff25e7c8b9 100644 --- a/analysis/src/completion_expressions.ml +++ b/analysis/src/completion_expressions.ml @@ -157,6 +157,7 @@ let rec traverse_expr (exp : Parsetree.expression) ~expr_path ~pos Completable.NVariantPayload { constructor_name = Longident.last txt; + constructor_path = Longident.flatten txt; item_num = 0; source_arity = 1; }; @@ -170,6 +171,7 @@ let rec traverse_expr (exp : Parsetree.expression) ~expr_path ~pos Completable.NVariantPayload { constructor_name = Longident.last txt; + constructor_path = Longident.flatten txt; item_num; source_arity = List.length args; }; @@ -180,6 +182,7 @@ let rec traverse_expr (exp : Parsetree.expression) ~expr_path ~pos Completable.NVariantPayload { constructor_name = Longident.last txt; + constructor_path = Longident.flatten txt; item_num = item_num + 1; source_arity = List.length args; }; diff --git a/analysis/src/completion_front_end.ml b/analysis/src/completion_front_end.ml index 8ad988f56b..2d46ed176d 100644 --- a/analysis/src/completion_front_end.ml +++ b/analysis/src/completion_front_end.ml @@ -504,6 +504,7 @@ let completion_with_parser1 ~debug ~offset ~pos_cursor ~kind_file item_num = index; source_arity = List.length patterns; constructor_name = Longident.last txt; + constructor_path = Longident.flatten txt; } :: pattern_path) ?context_path) diff --git a/analysis/src/completion_patterns.ml b/analysis/src/completion_patterns.ml index a9f5d508f1..28e0e980bf 100644 --- a/analysis/src/completion_patterns.ml +++ b/analysis/src/completion_patterns.ml @@ -212,6 +212,7 @@ and traverse_pattern (pat : Parsetree.pattern) ~pattern_path ~loc_has_cursor Completable.NVariantPayload { constructor_name = Longident.last txt; + constructor_path = Longident.flatten txt; item_num = 0; source_arity = 1; }; @@ -226,6 +227,7 @@ and traverse_pattern (pat : Parsetree.pattern) ~pattern_path ~loc_has_cursor Completable.NVariantPayload { constructor_name = Longident.last txt; + constructor_path = Longident.flatten txt; item_num; source_arity = List.length patterns; }; @@ -236,6 +238,7 @@ and traverse_pattern (pat : Parsetree.pattern) ~pattern_path ~loc_has_cursor Completable.NVariantPayload { constructor_name = Longident.last txt; + constructor_path = Longident.flatten txt; item_num = item_num + 1; source_arity = List.length patterns; }; diff --git a/analysis/src/shared_types.ml b/analysis/src/shared_types.ml index 5333f40832..802656983c 100644 --- a/analysis/src/shared_types.ml +++ b/analysis/src/shared_types.ml @@ -378,6 +378,9 @@ and completion_type = | Tvariant of { env: Query_env.t; constructors: Constructor.t list; + extension_envs: (Constructor.t * Query_env.t) list; + (** The environments of the files declaring extension + constructors, where their payloads are typed *) variant_decl: Types.type_declaration; variant_name: string; } @@ -606,6 +609,8 @@ module Completable = struct | NRecordBody of {seen_fields: string list} | NVariantPayload of { constructor_name: string; + constructor_path: string list; + (** The constructor as written, with its module path *) item_num: int; source_arity: int; } diff --git a/analysis/src/type_utils.ml b/analysis/src/type_utils.ml index 24bc7bee3b..df7fb6101b 100644 --- a/analysis/src/type_utils.ml +++ b/analysis/src/type_utils.ml @@ -361,27 +361,87 @@ let extract_function_type2 ?type_arg_context ~state ~env ~package typ = in loop ?type_arg_context ~env [] typ -(* The file being edited and the module path at the cursor, while a - completion request runs. Extension constructors are named relative to - it: unlike ordinary constructors they can't be found from the expected - type, so they have to be written so that they are in scope there. *) -let completion_scope : (File.t * string list * Pos.t) option ref = ref None - -let with_completion_scope ~file ~module_path ~pos f = +(* The file being edited, the module path at the cursor and the modules + opened there, most recent first, while a completion request runs. + Extension constructors are named relative to it: unlike ordinary + constructors they can't be found from the expected type, so they have to + be written so that they are in scope there. *) +let completion_scope : + (File.t * string list * Pos.t * string list list) option ref = + ref None + +let with_completion_scope ~file ~module_path ~pos ~opens f = let previous = !completion_scope in - completion_scope := Some (file, module_path, pos); + completion_scope := Some (file, module_path, pos, opens); Fun.protect ~finally:(fun () -> completion_scope := previous) f -(* Whether [c] is the constructor written [name] in a payload position, where - only the last part of a path such as [Sub.Flag] is kept. Extension - constructors are named by their module path, ordinary ones are not. *) -let constructor_has_name (c : Constructor.t) name = - c.cname.txt = name - || - match String.rindex_opt c.cname.txt '.' with - | Some i -> - String.sub c.cname.txt (i + 1) (String.length c.cname.txt - i - 1) = name - | None -> false +(* The constructor written [path] in a payload position, with the + environment its payload is typed in. Extension constructors are named by + their module path, so the one sharing the longest suffix with [path] is + meant. Of several equally good ones, the one that came into scope last + wins: through the most recent open naming its unwritten qualifier, else + with nothing left unwritten, the last declared shadowing the others. *) +let find_payload_constructor ~env ~constructors ~extension_envs path = + let opens = + match !completion_scope with + | Some (_, _, _, opens) -> opens + | None -> [] + in + let rec common_suffix a b = + match (a, b) with + | x :: a, y :: b when x = y -> 1 + common_suffix a b + | _ -> 0 + in + let rec ends_with l suffix = + l = suffix + || + match l with + | _ :: l -> ends_with l suffix + | [] -> false + in + let written = List.rev path in + let scored = + constructors + |> List.map (fun (c : Constructor.t) -> + let parts = List.rev (String.split_on_char '.' c.cname.txt) in + let score = common_suffix parts written in + let qualifier = List.rev (List.filteri (fun i _ -> i >= score) parts) in + (c, score, qualifier)) + in + let best = List.fold_left (fun m (_, score, _) -> max m score) 0 scored in + let candidates = + scored |> List.filter (fun (_, score, _) -> best > 0 && score = best) + in + (* How recently the constructor came into scope: the position in [opens] + of the module its unwritten qualifier names, or after all opens when + nothing is left unwritten. *) + let scope_rank qualifier = + let rec find i = function + | [] -> None + | o :: opens -> + if ends_with qualifier o || ends_with o qualifier then Some i + else find (i + 1) opens + in + if qualifier = [] then Some (List.length opens) else find 0 opens + in + let in_scope = + candidates + |> List.fold_left + (fun found (c, _, qualifier) -> + match (found, scope_rank qualifier) with + | Some (best, _), Some rank when rank <= best -> Some (rank, c) + | None, Some rank -> Some (rank, c) + | found, _ -> found) + None + in + let found = + match (in_scope, List.rev candidates) with + | Some (_, c), _ | None, (c, _, _) :: _ -> Some c + | None, [] -> None + in + found + |> Option.map (fun c -> + (c, List.assq_opt c extension_envs |> Option.value ~default:env)) (* The stamp under which [typ] is declared in [file]. *) let type_stamp (file : File.t) (typ : Type.t) = @@ -396,12 +456,13 @@ let type_stamp (file : File.t) (typ : Type.t) = type it extends by its path where it is declared: an identifier of the type's own file, or a path resolved from the extension's file. The constructors are named by their module path, since an extension - constructor can't be found from the expected type alone. *) + constructor can't be found from the expected type alone, and come with + the environment of their file, where their payloads are typed. *) let extension_constructors ~state ~package ~(current_file : File.t) ~(type_file : File.t) ~type_stamp ~type_name ~decl = let current_file, scope_path, cursor = match !completion_scope with - | Some (file, module_path, pos) -> (file, module_path, Some pos) + | Some (file, module_path, pos, _) -> (file, module_path, Some pos) | None -> (current_file, [], None) in (* An extension of the edited file is in scope from its declaration on. *) @@ -470,11 +531,12 @@ let extension_constructors ~state ~package ~(current_file : File.t) if file.uri = current_file.uri then relative path scope_path else Utils.flatten_any_namespace_in_path (file.module_name :: path) in - { - declared.item with - cname = {declared.item.cname with txt = String.concat "." path}; - type_decl = (type_name, decl); - }) + ( { + declared.item with + cname = {declared.item.cname with txt = String.concat "." path}; + type_decl = (type_name, decl); + }, + Query_env.from_file file )) let rec extract_type ?(print_opening_debug = true) ?(type_arg_context : type_arg_context option) @@ -556,6 +618,7 @@ let rec extract_type ?(print_opening_debug = true) { env = env_from_item; constructors; + extension_envs = []; variant_name = name.txt; variant_decl = decl; }, @@ -573,12 +636,13 @@ let rec extract_type ?(print_opening_debug = true) | [] when name.txt = "t" -> Some (TtypeT {env = env_from_item; path}, type_arg_context) | [] -> None - | constructors -> + | extension_envs -> Some ( Tvariant { env = env_from_item; - constructors; + constructors = List.map fst extension_envs; + extension_envs; variant_name = name.txt; variant_decl = decl; }, @@ -738,7 +802,13 @@ let extract_type_from_resolved_type (typ : Type.t) ~env ~full ~state = | Variant constructors -> Some (Tvariant - {env; constructors; variant_name = typ.name; variant_decl = typ.decl}) + { + env; + constructors; + extension_envs = []; + variant_name = typ.name; + variant_decl = typ.decl; + }) | Open when typ.decl.type_manifest = None -> ( match extension_constructors ~state ~package:full.package @@ -746,11 +816,16 @@ let extract_type_from_resolved_type (typ : Type.t) ~env ~full ~state = ~type_stamp:(type_stamp env.file typ) ~type_name:typ.name ~decl:typ.decl with | [] -> None - | constructors -> + | extension_envs -> Some (Tvariant - {env; constructors; variant_name = typ.name; variant_decl = typ.decl}) - ) + { + env; + constructors = List.map fst extension_envs; + extension_envs; + variant_name = typ.name; + variant_decl = typ.decl; + })) | Abstract _ | Open -> ( match typ.decl.type_manifest with | None -> None @@ -910,19 +985,19 @@ let rec resolve_nested ?type_arg_context ~env ~full ~state ~nested ?ctx |> extract_type ~env ~state ~package:full.package |> Utils.Option.flat_map (fun (t, type_arg_context) -> t |> resolve_nested ?type_arg_context ~env ~state ~full ~nested) - | ( NVariantPayload {constructor_name; item_num; source_arity}, - Tvariant {env; constructors} ) -> ( + | ( NVariantPayload + {constructor_name; constructor_path; item_num; source_arity}, + Tvariant {env; constructors; extension_envs} ) -> ( if Debug.verbose () then Printf.printf "[nested]--> trying to move into variant payload $%i of constructor \ '%s'\n" item_num constructor_name; match - constructors - |> List.find_opt (fun (c : Constructor.t) -> - constructor_has_name c constructor_name) + find_payload_constructor ~env ~constructors ~extension_envs + constructor_path with - | Some {args = Args args} -> ( + | Some ({args = Args args}, env) -> ( if Debug.verbose () then print_endline "[nested]--> found constructor (Args type)"; let item_num, nested = @@ -949,7 +1024,7 @@ let rec resolve_nested ?type_arg_context ~env ~full ~state ~nested ?ctx (List.length nested); typ |> resolve_nested ?type_arg_context ~env ~state ~full ~nested) ) - | Some {args = InlineRecord fields} when item_num = 0 -> + | Some ({args = InlineRecord fields}, env) when item_num = 0 -> if Debug.verbose () then print_endline "[nested]--> found constructor (inline record)"; TinlineRecord {env; fields} @@ -1051,14 +1126,13 @@ let rec resolve_nested_pattern_path (typ : inner_type) ~env ~full ~state ~nested | None -> None | Some typ -> TypeExpr typ |> resolve_nested_pattern_path ~env ~state ~full ~nested) - | ( NVariantPayload {constructor_name; item_num; source_arity}, - Tvariant {env; constructors} ) -> ( + | ( NVariantPayload {constructor_path; item_num; source_arity}, + Tvariant {env; constructors; extension_envs} ) -> ( match - constructors - |> List.find_opt (fun (constructor : Constructor.t) -> - constructor_has_name constructor constructor_name) + find_payload_constructor ~env ~constructors ~extension_envs + constructor_path with - | Some {args = Args args} -> ( + | Some ({args = Args args}, env) -> ( let item_num, nested = normalize_constructor_payload_path ~argument_count:(List.length args) ~source_arity ~item_num ~nested @@ -1068,10 +1142,10 @@ let rec resolve_nested_pattern_path (typ : inner_type) ~env ~full ~state ~nested TypeExpr typ |> resolve_nested_pattern_path ~env ~state ~full ~nested | None -> None) - | Some {args = InlineRecord fields} when item_num = 0 -> + | Some ({args = InlineRecord fields}, env) when item_num = 0 -> ExtractedType (TinlineRecord {env; fields}) |> resolve_nested_pattern_path ~env ~state ~full ~nested - | Some {args = InlineRecord _} | None -> None) + | Some ({args = InlineRecord _}, _) | None -> None) | ( NPolyvariantPayload {constructor_name; item_num; source_arity}, Tpolyvariant {env; constructors} ) -> ( match diff --git a/tests/analysis_tests/tests/src/CompletionExtensiblePayload.res b/tests/analysis_tests/tests/src/CompletionExtensiblePayload.res new file mode 100644 index 0000000000..03137fb875 --- /dev/null +++ b/tests/analysis_tests/tests/src/CompletionExtensiblePayload.res @@ -0,0 +1,102 @@ +// Payloads of extensions are typed where the extension is declared +type color = Red | Blue +type record = {c: color} +type CompletionExtensiblePayloadBase.attr += + | Color(color) + | Boxed({b: color}) + | Wrapped(record) + | Pair(color, CompletionExtensiblePayloadBase.shape) + | Sized(CompletionExtensiblePayloadThird.size) + +let v: CompletionExtensiblePayloadBase.attr = Color(Red) +type attr = CompletionExtensiblePayloadBase.attr + +// let x: attr = Color() +// ^com + +// switch v { | Color() } +// ^com + +// let x: attr = Boxed({b: }) +// ^com + +// switch v { | Wrapped({c: }) } +// ^com + +// let x: attr = Pair(Red, ) +// ^com + +// switch v { | Pair(Red, ) } +// ^com + +// let x: attr = Sized() +// ^com + +// switch v { | Sized() } +// ^com + +// let x: attr = CompletionExtensiblePayloadBase.Shaped() +// ^com + +// Same-named constructors are told apart by their module path +module A = { + type CompletionExtensiblePayloadBase.attr += Value(bool) +} +module B = { + type CompletionExtensiblePayloadBase.attr += Value(option) + // let x: attr = Value() + // ^com +} + +// Payload types local to a nested module +module M = { + type local = X | Y + type CompletionExtensiblePayloadBase.attr += InM(local) +} + +// let x: attr = M.InM() +// ^com + +// switch v { | M.InM() } +// ^com + +// let x: attr = A.Value() +// ^com + +// let x: attr = B.Value() +// ^com + +// let x: attr = B.Value(Some()) +// ^com + +// switch v { | A.Value() } +// ^com + +// switch v { | B.Value() } +// ^com + +// let x: attr = CompletionExtensiblePayloadBase.Value() +// ^com + +// let x: attr = CompletionExtensiblePayload.B.Value() +// ^com + +// switch v { | CompletionExtensiblePayloadBase.Value() } +// ^com + +// Unqualified, the constructor of the most recent open is meant +open A + +// let x: attr = Value() +// ^com + +// switch v { | Value() } +// ^com + +open B + +// let x: attr = Value() +// ^com + +// switch v { | Value() } +// ^com diff --git a/tests/analysis_tests/tests/src/CompletionExtensiblePayloadBase.res b/tests/analysis_tests/tests/src/CompletionExtensiblePayloadBase.res new file mode 100644 index 0000000000..6eb9e7f50a --- /dev/null +++ b/tests/analysis_tests/tests/src/CompletionExtensiblePayloadBase.res @@ -0,0 +1,3 @@ +type attr = .. +type shape = Circle | Square +type attr += Value(shape) | Shaped(shape) diff --git a/tests/analysis_tests/tests/src/CompletionExtensiblePayloadIface.res b/tests/analysis_tests/tests/src/CompletionExtensiblePayloadIface.res new file mode 100644 index 0000000000..ab1549deca --- /dev/null +++ b/tests/analysis_tests/tests/src/CompletionExtensiblePayloadIface.res @@ -0,0 +1,5 @@ +type attr = .. +type tone = Light | Dark +module Inner = { + type attr += Toned(tone) +} diff --git a/tests/analysis_tests/tests/src/CompletionExtensiblePayloadIface.resi b/tests/analysis_tests/tests/src/CompletionExtensiblePayloadIface.resi new file mode 100644 index 0000000000..e730cbab5f --- /dev/null +++ b/tests/analysis_tests/tests/src/CompletionExtensiblePayloadIface.resi @@ -0,0 +1,5 @@ +type attr = .. +type tone = Light | Dark +module Inner: { + type attr += Toned(tone) +} diff --git a/tests/analysis_tests/tests/src/CompletionExtensiblePayloadIfaceUse.res b/tests/analysis_tests/tests/src/CompletionExtensiblePayloadIfaceUse.res new file mode 100644 index 0000000000..793b1c5f73 --- /dev/null +++ b/tests/analysis_tests/tests/src/CompletionExtensiblePayloadIfaceUse.res @@ -0,0 +1,3 @@ +// let x: CompletionExtensiblePayloadIface.attr = CompletionExtensiblePayloadIface.Inner.Toned() +// ^com + diff --git a/tests/analysis_tests/tests/src/CompletionExtensiblePayloadThird.res b/tests/analysis_tests/tests/src/CompletionExtensiblePayloadThird.res new file mode 100644 index 0000000000..63a5cab1c8 --- /dev/null +++ b/tests/analysis_tests/tests/src/CompletionExtensiblePayloadThird.res @@ -0,0 +1 @@ +type size = Small | Large diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayload.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayload.res.txt new file mode 100644 index 0000000000..7e4aa4417b --- /dev/null +++ b/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayload.res.txt @@ -0,0 +1,891 @@ +Complete src/CompletionExtensiblePayload.res 13:23 +XXX Not found! +Completable: Cexpression Type[attr]->variantPayload::Color($0) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Type[attr] +Path attr +[ + { + "detail": "Red", + "documentation": { + "kind": "markdown", + "value": "```rescript\nRed\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Red", + "insertTextFormat": 2, + "kind": 4, + "label": "Red", + "tags": [] + }, + { + "detail": "Blue", + "documentation": { + "kind": "markdown", + "value": "```rescript\nBlue\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Blue", + "insertTextFormat": 2, + "kind": 4, + "label": "Blue", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 16:22 +pos_cursor:[16:22] pos_no_white:[16:21] Found pattern:[16:16->16:23] +Ppat_construct Color:[16:16->16:21] +pos_cursor:[16:22] pos_no_white:[16:21] Found pattern:[16:21->16:23] +Ppat_construct ():[16:21->16:23] +Completable: Cpattern Value[v]->variantPayload::Color($0) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[v] +Path v +[ + { + "detail": "Red", + "documentation": { + "kind": "markdown", + "value": "```rescript\nRed\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Red", + "insertTextFormat": 2, + "kind": 4, + "label": "Red", + "tags": [] + }, + { + "detail": "Blue", + "documentation": { + "kind": "markdown", + "value": "```rescript\nBlue\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Blue", + "insertTextFormat": 2, + "kind": 4, + "label": "Blue", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 19:27 +XXX Not found! +Completable: Cexpression Type[attr]->variantPayload::Boxed($0), recordField(b) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Type[attr] +Path attr +[ + { + "detail": "Red", + "documentation": { + "kind": "markdown", + "value": "```rescript\nRed\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Red", + "insertTextFormat": 2, + "kind": 4, + "label": "Red", + "tags": [] + }, + { + "detail": "Blue", + "documentation": { + "kind": "markdown", + "value": "```rescript\nBlue\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Blue", + "insertTextFormat": 2, + "kind": 4, + "label": "Blue", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 22:28 +pos_cursor:[22:28] pos_no_white:[22:26] Found pattern:[22:16->22:30] +Ppat_construct Wrapped:[22:16->22:23] +pos_cursor:[22:28] pos_no_white:[22:26] Found pattern:[22:24->22:29] +Completable: Cpattern Value[v]->variantPayload::Wrapped($0), recordField(c) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[v] +Path v +[ + { + "detail": "Red", + "documentation": { + "kind": "markdown", + "value": "```rescript\nRed\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Red", + "insertTextFormat": 2, + "kind": 4, + "label": "Red", + "tags": [] + }, + { + "detail": "Blue", + "documentation": { + "kind": "markdown", + "value": "```rescript\nBlue\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Blue", + "insertTextFormat": 2, + "kind": 4, + "label": "Blue", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 25:27 +XXX Not found! +Completable: Cexpression Type[attr]->variantPayload::Pair($1) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Type[attr] +Path attr +[ + { + "detail": "Circle", + "documentation": { + "kind": "markdown", + "value": "```rescript\nCircle\n```\n\n```rescript\ntype shape = Circle | Square\n```" + }, + "insertText": "Circle", + "insertTextFormat": 2, + "kind": 4, + "label": "Circle", + "tags": [] + }, + { + "detail": "Square", + "documentation": { + "kind": "markdown", + "value": "```rescript\nSquare\n```\n\n```rescript\ntype shape = Circle | Square\n```" + }, + "insertText": "Square", + "insertTextFormat": 2, + "kind": 4, + "label": "Square", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 28:26 +pos_cursor:[28:26] pos_no_white:[28:24] Found pattern:[28:16->28:27] +Ppat_construct Pair:[28:16->28:20] +Completable: Cpattern Value[v]->variantPayload::Pair($1) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[v] +Path v +[ + { + "detail": "Circle", + "documentation": { + "kind": "markdown", + "value": "```rescript\nCircle\n```\n\n```rescript\ntype shape = Circle | Square\n```" + }, + "insertText": "Circle", + "insertTextFormat": 2, + "kind": 4, + "label": "Circle", + "tags": [] + }, + { + "detail": "Square", + "documentation": { + "kind": "markdown", + "value": "```rescript\nSquare\n```\n\n```rescript\ntype shape = Circle | Square\n```" + }, + "insertText": "Square", + "insertTextFormat": 2, + "kind": 4, + "label": "Square", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 31:23 +XXX Not found! +Completable: Cexpression Type[attr]->variantPayload::Sized($0) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Type[attr] +Path attr +[ + { + "detail": "Small", + "documentation": { + "kind": "markdown", + "value": "```rescript\nSmall\n```\n\n```rescript\ntype size = Small | Large\n```" + }, + "insertText": "Small", + "insertTextFormat": 2, + "kind": 4, + "label": "Small", + "tags": [] + }, + { + "detail": "Large", + "documentation": { + "kind": "markdown", + "value": "```rescript\nLarge\n```\n\n```rescript\ntype size = Small | Large\n```" + }, + "insertText": "Large", + "insertTextFormat": 2, + "kind": 4, + "label": "Large", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 34:22 +pos_cursor:[34:22] pos_no_white:[34:21] Found pattern:[34:16->34:23] +Ppat_construct Sized:[34:16->34:21] +pos_cursor:[34:22] pos_no_white:[34:21] Found pattern:[34:21->34:23] +Ppat_construct ():[34:21->34:23] +Completable: Cpattern Value[v]->variantPayload::Sized($0) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[v] +Path v +[ + { + "detail": "Small", + "documentation": { + "kind": "markdown", + "value": "```rescript\nSmall\n```\n\n```rescript\ntype size = Small | Large\n```" + }, + "insertText": "Small", + "insertTextFormat": 2, + "kind": 4, + "label": "Small", + "tags": [] + }, + { + "detail": "Large", + "documentation": { + "kind": "markdown", + "value": "```rescript\nLarge\n```\n\n```rescript\ntype size = Small | Large\n```" + }, + "insertText": "Large", + "insertTextFormat": 2, + "kind": 4, + "label": "Large", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 37:56 +XXX Not found! +Completable: Cexpression Type[attr]->variantPayload::Shaped($0) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Type[attr] +Path attr +[ + { + "detail": "Circle", + "documentation": { + "kind": "markdown", + "value": "```rescript\nCircle\n```\n\n```rescript\ntype shape = Circle | Square\n```" + }, + "insertText": "Circle", + "insertTextFormat": 2, + "kind": 4, + "label": "Circle", + "tags": [] + }, + { + "detail": "Square", + "documentation": { + "kind": "markdown", + "value": "```rescript\nSquare\n```\n\n```rescript\ntype shape = Circle | Square\n```" + }, + "insertText": "Square", + "insertTextFormat": 2, + "kind": 4, + "label": "Square", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 46:25 +XXX Not found! +Completable: Cexpression Type[attr]->variantPayload::Value($0) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Type[attr] +Path attr +[ + { + "detail": "color", + "documentation": { + "kind": "markdown", + "value": "```rescript\ntype color = Red | Blue\n```" + }, + "kind": 12, + "label": "None", + "tags": [] + }, + { + "detail": "color", + "documentation": { + "kind": "markdown", + "value": "```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Some($0)", + "insertTextFormat": 2, + "kind": 12, + "label": "Some(_)", + "tags": [] + }, + { + "detail": "Red", + "documentation": { + "kind": "markdown", + "value": "```rescript\nRed\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Some(Red)", + "insertTextFormat": 2, + "kind": 4, + "label": "Some(Red)", + "tags": [] + }, + { + "detail": "Blue", + "documentation": { + "kind": "markdown", + "value": "```rescript\nBlue\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Some(Blue)", + "insertTextFormat": 2, + "kind": 4, + "label": "Some(Blue)", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 56:23 +XXX Not found! +Completable: Cexpression Type[attr]->variantPayload::InM($0) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Type[attr] +Path attr +[ + { + "detail": "X", + "documentation": { + "kind": "markdown", + "value": "```rescript\nX\n```\n\n```rescript\ntype local = X | Y\n```" + }, + "insertText": "X", + "insertTextFormat": 2, + "kind": 4, + "label": "X", + "tags": [] + }, + { + "detail": "Y", + "documentation": { + "kind": "markdown", + "value": "```rescript\nY\n```\n\n```rescript\ntype local = X | Y\n```" + }, + "insertText": "Y", + "insertTextFormat": 2, + "kind": 4, + "label": "Y", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 59:22 +pos_cursor:[59:22] pos_no_white:[59:21] Found pattern:[59:16->59:23] +Ppat_construct M.InM:[59:16->59:21] +pos_cursor:[59:22] pos_no_white:[59:21] Found pattern:[59:21->59:23] +Ppat_construct ():[59:21->59:23] +Completable: Cpattern Value[v]->variantPayload::InM($0) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[v] +Path v +[ + { + "detail": "X", + "documentation": { + "kind": "markdown", + "value": "```rescript\nX\n```\n\n```rescript\ntype local = X | Y\n```" + }, + "insertText": "X", + "insertTextFormat": 2, + "kind": 4, + "label": "X", + "tags": [] + }, + { + "detail": "Y", + "documentation": { + "kind": "markdown", + "value": "```rescript\nY\n```\n\n```rescript\ntype local = X | Y\n```" + }, + "insertText": "Y", + "insertTextFormat": 2, + "kind": 4, + "label": "Y", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 62:25 +XXX Not found! +Completable: Cexpression Type[attr]->variantPayload::Value($0) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Type[attr] +Path attr +[ + { "detail": "bool", "kind": 4, "label": "true", "tags": [] }, + { "detail": "bool", "kind": 4, "label": "false", "tags": [] } +] + +Complete src/CompletionExtensiblePayload.res 65:25 +XXX Not found! +Completable: Cexpression Type[attr]->variantPayload::Value($0) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Type[attr] +Path attr +[ + { + "detail": "color", + "documentation": { + "kind": "markdown", + "value": "```rescript\ntype color = Red | Blue\n```" + }, + "kind": 12, + "label": "None", + "tags": [] + }, + { + "detail": "color", + "documentation": { + "kind": "markdown", + "value": "```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Some($0)", + "insertTextFormat": 2, + "kind": 12, + "label": "Some(_)", + "tags": [] + }, + { + "detail": "Red", + "documentation": { + "kind": "markdown", + "value": "```rescript\nRed\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Some(Red)", + "insertTextFormat": 2, + "kind": 4, + "label": "Some(Red)", + "tags": [] + }, + { + "detail": "Blue", + "documentation": { + "kind": "markdown", + "value": "```rescript\nBlue\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Some(Blue)", + "insertTextFormat": 2, + "kind": 4, + "label": "Some(Blue)", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 68:30 +XXX Not found! +Completable: Cexpression Type[attr]->variantPayload::Value($0), variantPayload::Some($0) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Type[attr] +Path attr +[ + { + "detail": "Red", + "documentation": { + "kind": "markdown", + "value": "```rescript\nRed\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Red", + "insertTextFormat": 2, + "kind": 4, + "label": "Red", + "tags": [] + }, + { + "detail": "Blue", + "documentation": { + "kind": "markdown", + "value": "```rescript\nBlue\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Blue", + "insertTextFormat": 2, + "kind": 4, + "label": "Blue", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 71:24 +pos_cursor:[71:24] pos_no_white:[71:23] Found pattern:[71:16->71:25] +Ppat_construct A.Value:[71:16->71:23] +pos_cursor:[71:24] pos_no_white:[71:23] Found pattern:[71:23->71:25] +Ppat_construct ():[71:23->71:25] +Completable: Cpattern Value[v]->variantPayload::Value($0) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[v] +Path v +[ + { "detail": "bool", "kind": 4, "label": "true", "tags": [] }, + { "detail": "bool", "kind": 4, "label": "false", "tags": [] } +] + +Complete src/CompletionExtensiblePayload.res 74:24 +pos_cursor:[74:24] pos_no_white:[74:23] Found pattern:[74:16->74:25] +Ppat_construct B.Value:[74:16->74:23] +pos_cursor:[74:24] pos_no_white:[74:23] Found pattern:[74:23->74:25] +Ppat_construct ():[74:23->74:25] +Completable: Cpattern Value[v]->variantPayload::Value($0) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[v] +Path v +[ + { + "detail": "color", + "documentation": { + "kind": "markdown", + "value": "```rescript\ntype color = Red | Blue\n```" + }, + "kind": 12, + "label": "None", + "tags": [] + }, + { + "detail": "color", + "documentation": { + "kind": "markdown", + "value": "```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Some(${1:_})", + "insertTextFormat": 2, + "kind": 12, + "label": "Some(_)", + "tags": [] + }, + { + "detail": "Red", + "documentation": { + "kind": "markdown", + "value": "```rescript\nRed\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Some(Red)", + "insertTextFormat": 2, + "kind": 4, + "label": "Some(Red)", + "tags": [] + }, + { + "detail": "Blue", + "documentation": { + "kind": "markdown", + "value": "```rescript\nBlue\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Some(Blue)", + "insertTextFormat": 2, + "kind": 4, + "label": "Some(Blue)", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 77:55 +XXX Not found! +Completable: Cexpression Type[attr]->variantPayload::Value($0) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Type[attr] +Path attr +[ + { + "detail": "Circle", + "documentation": { + "kind": "markdown", + "value": "```rescript\nCircle\n```\n\n```rescript\ntype shape = Circle | Square\n```" + }, + "insertText": "Circle", + "insertTextFormat": 2, + "kind": 4, + "label": "Circle", + "tags": [] + }, + { + "detail": "Square", + "documentation": { + "kind": "markdown", + "value": "```rescript\nSquare\n```\n\n```rescript\ntype shape = Circle | Square\n```" + }, + "insertText": "Square", + "insertTextFormat": 2, + "kind": 4, + "label": "Square", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 80:53 +XXX Not found! +Completable: Cexpression Type[attr]->variantPayload::Value($0) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Type[attr] +Path attr +[ + { + "detail": "color", + "documentation": { + "kind": "markdown", + "value": "```rescript\ntype color = Red | Blue\n```" + }, + "kind": 12, + "label": "None", + "tags": [] + }, + { + "detail": "color", + "documentation": { + "kind": "markdown", + "value": "```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Some($0)", + "insertTextFormat": 2, + "kind": 12, + "label": "Some(_)", + "tags": [] + }, + { + "detail": "Red", + "documentation": { + "kind": "markdown", + "value": "```rescript\nRed\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Some(Red)", + "insertTextFormat": 2, + "kind": 4, + "label": "Some(Red)", + "tags": [] + }, + { + "detail": "Blue", + "documentation": { + "kind": "markdown", + "value": "```rescript\nBlue\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Some(Blue)", + "insertTextFormat": 2, + "kind": 4, + "label": "Some(Blue)", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 83:54 +pos_cursor:[83:54] pos_no_white:[83:53] Found pattern:[83:16->83:55] +Ppat_construct CompletionExtensiblePayloadBase.Value:[83:16->83:53] +pos_cursor:[83:54] pos_no_white:[83:53] Found pattern:[83:53->83:55] +Ppat_construct ():[83:53->83:55] +Completable: Cpattern Value[v]->variantPayload::Value($0) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[v] +Path v +[ + { + "detail": "Circle", + "documentation": { + "kind": "markdown", + "value": "```rescript\nCircle\n```\n\n```rescript\ntype shape = Circle | Square\n```" + }, + "insertText": "Circle", + "insertTextFormat": 2, + "kind": 4, + "label": "Circle", + "tags": [] + }, + { + "detail": "Square", + "documentation": { + "kind": "markdown", + "value": "```rescript\nSquare\n```\n\n```rescript\ntype shape = Circle | Square\n```" + }, + "insertText": "Square", + "insertTextFormat": 2, + "kind": 4, + "label": "Square", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 89:23 +XXX Not found! +Completable: Cexpression Type[attr]->variantPayload::Value($0) +Raw opens: 1 A.place holder +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 2 Stdlib CompletionExtensiblePayload +ContextPath Type[attr] +Path attr +[ + { "detail": "bool", "kind": 4, "label": "true", "tags": [] }, + { "detail": "bool", "kind": 4, "label": "false", "tags": [] } +] + +Complete src/CompletionExtensiblePayload.res 92:22 +pos_cursor:[92:22] pos_no_white:[92:21] Found pattern:[92:16->92:23] +Ppat_construct Value:[92:16->92:21] +pos_cursor:[92:22] pos_no_white:[92:21] Found pattern:[92:21->92:23] +Ppat_construct ():[92:21->92:23] +Completable: Cpattern Value[v]->variantPayload::Value($0) +Raw opens: 1 A.place holder +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 2 Stdlib CompletionExtensiblePayload +ContextPath Value[v] +Path v +[ + { "detail": "bool", "kind": 4, "label": "true", "tags": [] }, + { "detail": "bool", "kind": 4, "label": "false", "tags": [] } +] + +Complete src/CompletionExtensiblePayload.res 97:23 +XXX Not found! +Completable: Cexpression Type[attr]->variantPayload::Value($0) +Raw opens: 2 B.place holder ... A.place holder +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 3 Stdlib CompletionExtensiblePayload CompletionExtensiblePayload +ContextPath Type[attr] +Path attr +[ + { + "detail": "color", + "documentation": { + "kind": "markdown", + "value": "```rescript\ntype color = Red | Blue\n```" + }, + "kind": 12, + "label": "None", + "tags": [] + }, + { + "detail": "color", + "documentation": { + "kind": "markdown", + "value": "```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Some($0)", + "insertTextFormat": 2, + "kind": 12, + "label": "Some(_)", + "tags": [] + }, + { + "detail": "Red", + "documentation": { + "kind": "markdown", + "value": "```rescript\nRed\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Some(Red)", + "insertTextFormat": 2, + "kind": 4, + "label": "Some(Red)", + "tags": [] + }, + { + "detail": "Blue", + "documentation": { + "kind": "markdown", + "value": "```rescript\nBlue\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Some(Blue)", + "insertTextFormat": 2, + "kind": 4, + "label": "Some(Blue)", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 100:22 +pos_cursor:[100:22] pos_no_white:[100:21] Found pattern:[100:16->100:23] +Ppat_construct Value:[100:16->100:21] +pos_cursor:[100:22] pos_no_white:[100:21] Found pattern:[100:21->100:23] +Ppat_construct ():[100:21->100:23] +Completable: Cpattern Value[v]->variantPayload::Value($0) +Raw opens: 2 B.place holder ... A.place holder +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 3 Stdlib CompletionExtensiblePayload CompletionExtensiblePayload +ContextPath Value[v] +Path v +[ + { + "detail": "color", + "documentation": { + "kind": "markdown", + "value": "```rescript\ntype color = Red | Blue\n```" + }, + "kind": 12, + "label": "None", + "tags": [] + }, + { + "detail": "color", + "documentation": { + "kind": "markdown", + "value": "```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Some(${1:_})", + "insertTextFormat": 2, + "kind": 12, + "label": "Some(_)", + "tags": [] + }, + { + "detail": "Red", + "documentation": { + "kind": "markdown", + "value": "```rescript\nRed\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Some(Red)", + "insertTextFormat": 2, + "kind": 4, + "label": "Some(Red)", + "tags": [] + }, + { + "detail": "Blue", + "documentation": { + "kind": "markdown", + "value": "```rescript\nBlue\n```\n\n```rescript\ntype color = Red | Blue\n```" + }, + "insertText": "Some(Blue)", + "insertTextFormat": 2, + "kind": 4, + "label": "Some(Blue)", + "tags": [] + } +] + diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayloadBase.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayloadBase.res.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayloadIface.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayloadIface.res.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayloadIface.resi.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayloadIface.resi.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayloadIfaceUse.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayloadIfaceUse.res.txt new file mode 100644 index 0000000000..4d0bd216ac --- /dev/null +++ b/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayloadIfaceUse.res.txt @@ -0,0 +1,34 @@ +Complete src/CompletionExtensiblePayloadIfaceUse.res 0:95 +XXX Not found! +Completable: Cexpression Type[CompletionExtensiblePayloadIface, attr]->variantPayload::Toned($0) +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Type[CompletionExtensiblePayloadIface, attr] +Path CompletionExtensiblePayloadIface.attr +[ + { + "detail": "Light", + "documentation": { + "kind": "markdown", + "value": "```rescript\nLight\n```\n\n```rescript\ntype tone = Light | Dark\n```" + }, + "insertText": "Light", + "insertTextFormat": 2, + "kind": 4, + "label": "Light", + "tags": [] + }, + { + "detail": "Dark", + "documentation": { + "kind": "markdown", + "value": "```rescript\nDark\n```\n\n```rescript\ntype tone = Light | Dark\n```" + }, + "insertText": "Dark", + "insertTextFormat": 2, + "kind": 4, + "label": "Dark", + "tags": [] + } +] + diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayloadThird.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayloadThird.res.txt new file mode 100644 index 0000000000..e69de29bb2 From d56bea18d4d74ff9d7edf74fce9750248245e3fe Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Thu, 29 Oct 2026 10:33:23 +0000 Subject: [PATCH 08/10] Look up unqualified extension payloads in the scope at the cursor Codex review on #9085: ranking same-named extension constructors let an open outrank a local extension declared after it, so after `open A` and a later local `Value(...)`, completion used `A.Value`'s payload; and an extension inside a module type, `module type S = { type attr += Phantom }`, was offered as `S.Phantom` and added to exhaustive switches. A qualified constructor is now the one its written path names, and an unqualified one is looked up in the completion scope as the compiler does, which now records extension constructors, the most recent declaration or open winning. Extensions in module types are skipped. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Kbf3gGkPma9LAZimuQjnrP Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo Signed-off-by: Christoph Knittel --- analysis/src/completion_back_end.ml | 7 +- analysis/src/completion_front_end.ml | 6 + analysis/src/type_utils.ml | 129 ++++--- .../tests/src/CompletionExtensiblePayload.res | 20 + .../CompletionExtensiblePayload.res.txt | 365 ++++++++++++++++++ 5 files changed, 459 insertions(+), 68 deletions(-) diff --git a/analysis/src/completion_back_end.ml b/analysis/src/completion_back_end.ml index 85c3793d22..5290b691e7 100644 --- a/analysis/src/completion_back_end.ml +++ b/analysis/src/completion_back_end.ml @@ -2673,12 +2673,7 @@ let module_path_at ~(full : full) ~pos = let process_completable ~state ~debug ~full ~scope ~env ~pos ~for_hover completable = - (* A raw open ends in a placeholder for the name it qualifies. *) - let opens = - Scope.get_raw_opens scope - |> List.map (List.filter (fun name -> name <> "place holder")) - in Type_utils.with_completion_scope ~file:full.file - ~module_path:(module_path_at ~full ~pos) ~pos ~opens (fun () -> + ~module_path:(module_path_at ~full ~pos) ~pos ~scope (fun () -> process_completable_in_scope ~state ~debug ~full ~scope ~env ~pos ~for_hover completable) diff --git a/analysis/src/completion_front_end.ml b/analysis/src/completion_front_end.ml index 2d46ed176d..0d5df19aea 100644 --- a/analysis/src/completion_front_end.ml +++ b/analysis/src/completion_front_end.ml @@ -745,6 +745,12 @@ let completion_with_parser1 ~debug ~offset ~pos_cursor ~kind_file | Pstr_open {popen_lid} -> scope := !scope |> Scope.add_open ~lid:popen_lid.txt | Pstr_primitive vd -> scope_value_description vd + | Pstr_typext {ptyext_constructors} -> + ptyext_constructors + |> List.iter (fun (ext : Parsetree.extension_constructor) -> + scope := + !scope + |> Scope.add_constructor ~name:ext.pext_name.txt ~loc:ext.pext_loc) | Pstr_value (rec_flag, bindings) -> if rec_flag = Recursive then bindings |> List.iter scope_value_binding; bindings |> List.iter (fun vb -> iterator.value_binding iterator vb); diff --git a/analysis/src/type_utils.ml b/analysis/src/type_utils.ml index df7fb6101b..a16968f2fd 100644 --- a/analysis/src/type_utils.ml +++ b/analysis/src/type_utils.ml @@ -361,36 +361,31 @@ let extract_function_type2 ?type_arg_context ~state ~env ~package typ = in loop ?type_arg_context ~env [] typ -(* The file being edited, the module path at the cursor and the modules - opened there, most recent first, while a completion request runs. - Extension constructors are named relative to it: unlike ordinary - constructors they can't be found from the expected type, so they have to - be written so that they are in scope there. *) +(* The file being edited, the module path at the cursor and the scope there, + while a completion request runs. Extension constructors are named + relative to it: unlike ordinary constructors they can't be found from the + expected type, so they have to be written so that they are in scope + there. *) let completion_scope : - (File.t * string list * Pos.t * string list list) option ref = + (File.t * string list * Pos.t * Scope_types.item list) option ref = ref None -let with_completion_scope ~file ~module_path ~pos ~opens f = +let with_completion_scope ~file ~module_path ~pos ~scope f = let previous = !completion_scope in - completion_scope := Some (file, module_path, pos, opens); + completion_scope := Some (file, module_path, pos, scope); Fun.protect ~finally:(fun () -> completion_scope := previous) f (* The constructor written [path] in a payload position, with the - environment its payload is typed in. Extension constructors are named by - their module path, so the one sharing the longest suffix with [path] is - meant. Of several equally good ones, the one that came into scope last - wins: through the most recent open naming its unwritten qualifier, else - with nothing left unwritten, the last declared shadowing the others. *) -let find_payload_constructor ~env ~constructors ~extension_envs path = - let opens = - match !completion_scope with - | Some (_, _, _, opens) -> opens - | None -> [] - in - let rec common_suffix a b = - match (a, b) with - | x :: a, y :: b when x = y -> 1 + common_suffix a b - | _ -> 0 + environment its payload is typed in. An ordinary constructor is found by + its name. An extension constructor is named by its module path from the + cursor: written qualified, it is the one that path names; unqualified, it + is looked up in the scope at the cursor as the compiler does, the most + recent declaration or open bringing a constructor of that name and type + into scope. *) +let find_payload_constructor ~(env : Query_env.t) ~constructors ~extension_envs + path = + let with_env c = + (c, List.assq_opt c extension_envs |> Option.value ~default:env) in let rec ends_with l suffix = l = suffix @@ -399,49 +394,50 @@ let find_payload_constructor ~env ~constructors ~extension_envs path = | _ :: l -> ends_with l suffix | [] -> false in - let written = List.rev path in - let scored = + let parts (c : Constructor.t) = String.split_on_char '.' c.cname.txt in + (* The constructors [path] names, possibly from an opened module, or + written from further out than the cursor. *) + let named path = constructors - |> List.map (fun (c : Constructor.t) -> - let parts = List.rev (String.split_on_char '.' c.cname.txt) in - let score = common_suffix parts written in - let qualifier = List.rev (List.filteri (fun i _ -> i >= score) parts) in - (c, score, qualifier)) - in - let best = List.fold_left (fun m (_, score, _) -> max m score) 0 scored in - let candidates = - scored |> List.filter (fun (_, score, _) -> best > 0 && score = best) - in - (* How recently the constructor came into scope: the position in [opens] - of the module its unwritten qualifier names, or after all opens when - nothing is left unwritten. *) - let scope_rank qualifier = - let rec find i = function - | [] -> None - | o :: opens -> - if ends_with qualifier o || ends_with o qualifier then Some i - else find (i + 1) opens - in - if qualifier = [] then Some (List.length opens) else find 0 opens - in - let in_scope = - candidates - |> List.fold_left - (fun found (c, _, qualifier) -> - match (found, scope_rank qualifier) with - | Some (best, _), Some rank when rank <= best -> Some (rank, c) - | None, Some rank -> Some (rank, c) - | found, _ -> found) - None + |> List.filter (fun c -> + ends_with (parts c) path || ends_with path (parts c)) in + let exact path = constructors |> List.find_opt (fun c -> parts c = path) in + let last l = List.nth_opt l (List.length l - 1) in let found = - match (in_scope, List.rev candidates) with - | Some (_, c), _ | None, (c, _, _) :: _ -> Some c - | None, [] -> None + match (extension_envs, path, !completion_scope) with + | [], _, _ -> + constructors + |> List.find_opt (fun (c : Constructor.t) -> Some c.cname.txt = last path) + | _, [name], Some ((file : File.t), _, _, scope) -> + let declared_at loc (c : Constructor.t) = + (snd (with_env c) : Query_env.t).file.uri = file.uri + && Loc.has_pos ~pos:(Loc.start c.cname.loc) loc + in + let rec lookup (scope : Scope_types.item list) = + match scope with + | [] -> exact path + | Constructor (n, loc) :: scope when n = name -> ( + match constructors |> List.find_opt (declared_at loc) with + | Some c -> Some c + | None -> lookup scope) + | Open opened :: scope -> ( + (* A raw open ends in a placeholder for the name it qualifies. *) + let opened = + List.filteri (fun i _ -> i < List.length opened - 1) opened + in + match last (named (opened @ path)) with + | Some c -> Some c + | None -> lookup scope) + | _ :: scope -> lookup scope + in + lookup scope + | _ -> ( + match exact path with + | Some c -> Some c + | None -> last (named path)) in - found - |> Option.map (fun c -> - (c, List.assq_opt c extension_envs |> Option.value ~default:env)) + found |> Option.map with_env (* The stamp under which [typ] is declared in [file]. *) let type_stamp (file : File.t) (typ : Type.t) = @@ -465,6 +461,14 @@ let extension_constructors ~state ~package ~(current_file : File.t) | Some (file, module_path, pos, _) -> (file, module_path, Some pos) | None -> (current_file, [], None) in + (* An extension in a module type declares no constructor. *) + let rec in_module_type (path : Module_path.t) = + match path with + | ExportedModule {is_type = true} -> true + | ExportedModule {module_path} | IncludedModule (_, module_path) -> + in_module_type module_path + | File _ | NotVisible -> false + in (* An extension of the edited file is in scope from its declaration on. *) let declared_before_cursor (file : File.t) (declared : _ Declared.t) = match cursor with @@ -502,6 +506,7 @@ let extension_constructors ~state ~package ~(current_file : File.t) name = type_name && ext_decl.type_kind = Type_open && declared_before_cursor file declared + && (not (in_module_type declared.module_path)) && extends file ext_decl then Hashtbl.replace found (file.uri, stamp) (file, declared))); Hashtbl.to_seq_values found diff --git a/tests/analysis_tests/tests/src/CompletionExtensiblePayload.res b/tests/analysis_tests/tests/src/CompletionExtensiblePayload.res index 03137fb875..133b97961c 100644 --- a/tests/analysis_tests/tests/src/CompletionExtensiblePayload.res +++ b/tests/analysis_tests/tests/src/CompletionExtensiblePayload.res @@ -100,3 +100,23 @@ open B // switch v { | Value() } // ^com + +// A local declaration after an open shadows the opened constructor +type CompletionExtensiblePayloadBase.attr += Value(CompletionExtensiblePayloadThird.size) + +// let x: attr = Value() +// ^com + +// switch v { | Value() } +// ^com + +// An extension in a module type declares no constructor +module type S = { + type CompletionExtensiblePayloadBase.attr += Phantom +} + +// let x: attr = P +// ^com + +// switch v +// ^com diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayload.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayload.res.txt index 7e4aa4417b..9d6129f323 100644 --- a/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayload.res.txt +++ b/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayload.res.txt @@ -889,3 +889,368 @@ Path v } ] +Complete src/CompletionExtensiblePayload.res 106:23 +XXX Not found! +Completable: Cexpression Type[attr]->variantPayload::Value($0) +Raw opens: 2 B.place holder ... A.place holder +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 3 Stdlib CompletionExtensiblePayload CompletionExtensiblePayload +ContextPath Type[attr] +Path attr +[ + { + "detail": "Small", + "documentation": { + "kind": "markdown", + "value": "```rescript\nSmall\n```\n\n```rescript\ntype size = Small | Large\n```" + }, + "insertText": "Small", + "insertTextFormat": 2, + "kind": 4, + "label": "Small", + "tags": [] + }, + { + "detail": "Large", + "documentation": { + "kind": "markdown", + "value": "```rescript\nLarge\n```\n\n```rescript\ntype size = Small | Large\n```" + }, + "insertText": "Large", + "insertTextFormat": 2, + "kind": 4, + "label": "Large", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 109:22 +pos_cursor:[109:22] pos_no_white:[109:21] Found pattern:[109:16->109:23] +Ppat_construct Value:[109:16->109:21] +pos_cursor:[109:22] pos_no_white:[109:21] Found pattern:[109:21->109:23] +Ppat_construct ():[109:21->109:23] +Completable: Cpattern Value[v]->variantPayload::Value($0) +Raw opens: 2 B.place holder ... A.place holder +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 3 Stdlib CompletionExtensiblePayload CompletionExtensiblePayload +ContextPath Value[v] +Path v +[ + { + "detail": "Small", + "documentation": { + "kind": "markdown", + "value": "```rescript\nSmall\n```\n\n```rescript\ntype size = Small | Large\n```" + }, + "insertText": "Small", + "insertTextFormat": 2, + "kind": 4, + "label": "Small", + "tags": [] + }, + { + "detail": "Large", + "documentation": { + "kind": "markdown", + "value": "```rescript\nLarge\n```\n\n```rescript\ntype size = Small | Large\n```" + }, + "insertText": "Large", + "insertTextFormat": 2, + "kind": 4, + "label": "Large", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 117:18 +XXX Not found! +Completable: Cexpression Type[attr]=P +Raw opens: 2 B.place holder ... A.place holder +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 3 Stdlib CompletionExtensiblePayload CompletionExtensiblePayload +ContextPath Type[attr] +Path attr +[ + { + "detail": "Pair(color, CompletionExtensiblePayloadBase.shape)", + "documentation": { + "kind": "markdown", + "value": "```rescript\nPair(color, CompletionExtensiblePayloadBase.shape)\n```\n\n```rescript\ntype attr = ..\n```" + }, + "insertText": "Pair(${1:_}, ${2:_})", + "insertTextFormat": 2, + "kind": 4, + "label": "Pair(_, _)", + "sortText": "A Pair(_, _)", + "tags": [] + }, + { "detail": "module Promise", "kind": 9, "label": "Promise", "tags": [] }, + { "detail": "module Pair", "kind": 9, "label": "Pair", "tags": [] }, + { + "data": { + "modulePath": "Patterns", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Patterns", + "kind": 9, + "label": "Patterns", + "tags": [] + }, + { + "data": { + "modulePath": "Pervasives", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Pervasives", + "kind": 9, + "label": "Pervasives", + "tags": [] + }, + { + "data": { + "modulePath": "PolyRec", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module PolyRec", + "kind": 9, + "label": "PolyRec", + "tags": [] + }, + { + "data": { + "modulePath": "PrepareRename", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module PrepareRename", + "kind": 9, + "label": "PrepareRename", + "tags": [] + }, + { + "data": { + "modulePath": "Primitive_array", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Primitive_array", + "kind": 9, + "label": "Primitive_array", + "tags": [] + }, + { + "data": { + "modulePath": "Primitive_array_extern", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Primitive_array_extern", + "kind": 9, + "label": "Primitive_array_extern", + "tags": [] + }, + { + "data": { + "modulePath": "Primitive_bigint", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Primitive_bigint", + "kind": 9, + "label": "Primitive_bigint", + "tags": [] + }, + { + "data": { + "modulePath": "Primitive_bool", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Primitive_bool", + "kind": 9, + "label": "Primitive_bool", + "tags": [] + }, + { + "data": { + "modulePath": "Primitive_dict", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Primitive_dict", + "kind": 9, + "label": "Primitive_dict", + "tags": [] + }, + { + "data": { + "modulePath": "Primitive_exceptions", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Primitive_exceptions", + "kind": 9, + "label": "Primitive_exceptions", + "tags": [] + }, + { + "data": { + "modulePath": "Primitive_float", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Primitive_float", + "kind": 9, + "label": "Primitive_float", + "tags": [] + }, + { + "data": { + "modulePath": "Primitive_float_extern", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Primitive_float_extern", + "kind": 9, + "label": "Primitive_float_extern", + "tags": [] + }, + { + "data": { + "modulePath": "Primitive_hash", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Primitive_hash", + "kind": 9, + "label": "Primitive_hash", + "tags": [] + }, + { + "data": { + "modulePath": "Primitive_int", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Primitive_int", + "kind": 9, + "label": "Primitive_int", + "tags": [] + }, + { + "data": { + "modulePath": "Primitive_int_extern", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Primitive_int_extern", + "kind": 9, + "label": "Primitive_int_extern", + "tags": [] + }, + { + "data": { + "modulePath": "Primitive_js_extern", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Primitive_js_extern", + "kind": 9, + "label": "Primitive_js_extern", + "tags": [] + }, + { + "data": { + "modulePath": "Primitive_list", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Primitive_list", + "kind": 9, + "label": "Primitive_list", + "tags": [] + }, + { + "data": { + "modulePath": "Primitive_module", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Primitive_module", + "kind": 9, + "label": "Primitive_module", + "tags": [] + }, + { + "data": { + "modulePath": "Primitive_object", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Primitive_object", + "kind": 9, + "label": "Primitive_object", + "tags": [] + }, + { + "data": { + "modulePath": "Primitive_object_extern", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Primitive_object_extern", + "kind": 9, + "label": "Primitive_object_extern", + "tags": [] + }, + { + "data": { + "modulePath": "Primitive_option", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Primitive_option", + "kind": 9, + "label": "Primitive_option", + "tags": [] + }, + { + "data": { + "modulePath": "Primitive_promise", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Primitive_promise", + "kind": 9, + "label": "Primitive_promise", + "tags": [] + }, + { + "data": { + "modulePath": "Primitive_string", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Primitive_string", + "kind": 9, + "label": "Primitive_string", + "tags": [] + }, + { + "data": { + "modulePath": "Primitive_string_extern", + "filePath": "src/CompletionExtensiblePayload.res" + }, + "detail": "module Primitive_string_extern", + "kind": 9, + "label": "Primitive_string_extern", + "tags": [] + } +] + +Complete src/CompletionExtensiblePayload.res 120:11 +XXX Not found! +Completable: CexhaustiveSwitch Value[v] +Raw opens: 2 B.place holder ... A.place holder +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 3 Stdlib CompletionExtensiblePayload CompletionExtensiblePayload +ContextPath Value[v] +Path v +[ + { + "detail": "CompletionExtensiblePayloadBase.attr", + "kind": 12, + "label": "v", + "tags": [] + }, + { + "detail": "insert exhaustive switch for value", + "filterText": "v", + "insertText": "v {\n | Color(_) => ${1:%todo}\n | Boxed(_) => ${2:%todo}\n | Wrapped(_) => ${3:%todo}\n | Pair(_) => ${4:%todo}\n | Sized(_) => ${5:%todo}\n | A.Value(_) => ${6:%todo}\n | B.Value(_) => ${7:%todo}\n | M.InM(_) => ${8:%todo}\n | Value(_) => ${9:%todo}\n | CompletionExtensiblePayloadBase.Value(_) => ${10:%todo}\n | CompletionExtensiblePayloadBase.Shaped(_) => ${11:%todo}\n | _ => ${12:%todo}\n }", + "insertTextFormat": 2, + "kind": 15, + "label": "v (exhaustive switch)", + "tags": [] + } +] + From fb97e507450a1294bc1f456359c5587e6bfa71c4 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Thu, 29 Oct 2026 18:02:42 +0000 Subject: [PATCH 09/10] Keep a catch-all when expanding or switching on an open type Why: Codex review on #9085: now that an open type extracts as a variant, the Expand catch-all and Exhaustive switch code actions replaced or built the switch from the constructors known here, dropping the `_` that matches constructors added in other files. Codeaction modified (Expand catch-all): Example: type attr = .. type attr += Str(string) | Num(int) let f = (value: attr) => switch value { | Str(_) => "str" | _ => "other" } Before (text): the `_` is replaced by Num(_) After (text): the `_` is replaced by Num(_) | _ Codeaction modified (Exhaustive switch): Example: type attr = .. type attr += Str(string) | Num(int) let value: attr = Str("s") value Before (rescript): switch value { | Str(_) => %todo | Num(_) => %todo } After (rescript): switch value { | Str(_) => %todo | Num(_) => %todo | _ => %todo } Review: Why keep the `_` as an alternative instead of a separate case? Replacing only the pattern needs no copy of the case body, and the expanded or-pattern lists the known constructors for splitting by hand. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo Signed-off-by: Christoph Knittel --- analysis/src/type_utils.ml | 24 ++++--- analysis/src/xform.ml | 24 +++++-- .../tests/src/CompletionExtensibleVariant.res | 18 +++++ .../CompletionExtensibleVariant.res.txt | 68 +++++++++++++++++++ 4 files changed, 120 insertions(+), 14 deletions(-) diff --git a/analysis/src/type_utils.ml b/analysis/src/type_utils.ml index a16968f2fd..22a71ff8e5 100644 --- a/analysis/src/type_utils.ml +++ b/analysis/src/type_utils.ml @@ -1290,15 +1290,23 @@ module Codegen = struct = match extracted_type with | Tvariant v -> + (* Other files can extend an open type, so only a catch-all case + makes the switch exhaustive. *) + let catch_all = + match v.variant_decl.type_kind with + | Type_open -> [any ()] + | Type_abstract | Type_record _ | Type_variant _ -> [] + in Some - (v.constructors - |> List.map (fun (c : Shared_types.Constructor.t) -> - mk_construct_pat - ?payload: - (match c.args with - | Args [] -> None - | _ -> Some (any ())) - c.cname.txt)) + ((v.constructors + |> List.map (fun (c : Shared_types.Constructor.t) -> + mk_construct_pat + ?payload: + (match c.args with + | Args [] -> None + | _ -> Some (any ())) + c.cname.txt)) + @ catch_all) | Tpolyvariant v -> Some (v.constructors diff --git a/analysis/src/xform.ml b/analysis/src/xform.ml index 394f7e9e1f..e4cacd8697 100644 --- a/analysis/src/xform.ml +++ b/analysis/src/xform.ml @@ -406,6 +406,13 @@ module Expand_catch_all_for_variants = struct in {Ast_iterator.default_iterator with expr} + (* Other files can extend an open type, so its expansion keeps a + catch-all alternative after the constructors known here. *) + let open_type_catch_all (decl : Types.type_declaration) = + match decl.type_kind with + | Type_open -> ["_"] + | Type_abstract | Type_record _ | Type_variant _ -> [] + let xform ~state ~source ~kind_file ~path ~pos ~full ~structure ~code_actions ~debug = let result = ref None in @@ -445,7 +452,7 @@ module Expand_catch_all_for_variants = struct |> extract_type_from_expr ~state ~debug ~source ~kind_file ~full ~pos:(Pos.of_lexing switch_expr.pexp_loc.loc_end) with - | Some (Tvariant {constructors}) -> + | Some (Tvariant {constructors; variant_decl}) -> let missing_constructors = constructors |> List.filter (fun (c : Shared_types.Constructor.t) -> @@ -453,13 +460,14 @@ module Expand_catch_all_for_variants = struct in if List.length missing_constructors > 0 then let new_text = - missing_constructors + (missing_constructors |> List.map (fun (c : Shared_types.Constructor.t) -> c.cname.txt ^ match c.args with | Args [] -> "" - | Args _ | InlineRecord _ -> "(_)") + | Args _ | InlineRecord _ -> "(_)")) + @ open_type_catch_all variant_decl |> String.concat " | " in let range = Loc.range_of_loc catch_all_case.pc_lhs.ppat_loc in @@ -549,9 +557,13 @@ module Expand_catch_all_for_variants = struct if List.length missing_constructors > 0 || not has_none_case then let new_text = "Some(" - ^ (missing_constructors - |> List.map (fun (name, has_args) -> - name ^ if has_args then "(_)" else "") + ^ (((missing_constructors + |> List.map (fun (name, has_args) -> + name ^ if has_args then "(_)" else "")) + @ + match variant with + | Tvariant {variant_decl} -> open_type_catch_all variant_decl + | _ -> []) |> String.concat " | ") ^ ")" in diff --git a/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res b/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res index c031639d1b..3c313c2c15 100644 --- a/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res +++ b/tests/analysis_tests/tests/src/CompletionExtensibleVariant.res @@ -51,3 +51,21 @@ type attr += Later // Payload of a qualified extension constructor // let toggled: attr = Sub.Toggle(t // ^com + +// Expanding the catch-all of an open type +let _x = switch value { +| Str(_) => "str" +| _ => "other" +// ^xfm +} + +let optValue = Some(value) +let _y = switch optValue { +| Some(Str(_)) => "str" +| _ => "other" +// ^xfm +} + +// An exhaustive switch of an open type +// value +// ^xfm^ diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt index f3e9907dcc..e74d7a10f9 100644 --- a/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt +++ b/tests/analysis_tests/tests/src/expected/CompletionExtensibleVariant.res.txt @@ -245,3 +245,71 @@ Path attr } ] +Xform src/CompletionExtensibleVariant.res 57:4 +pos_cursor:[55:16] pos_no_white:[55:14] Found expr:[55:9->59:1] +Completable: Cpath Value[value] +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[value] +Path value +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +Hit: Expand catch-all + +TextDocumentEdit: CompletionExtensibleVariant.res +{ + "end": { "character": 3, "line": 57 }, + "start": { "character": 2, "line": 57 } +} +newText: + <--here + Num(_) | Sub.Flag | Sub.Toggle(_) | Later | _ + +Xform src/CompletionExtensibleVariant.res 64:4 +pos_cursor:[62:16] pos_no_white:[62:14] Found expr:[62:9->66:1] +Completable: Cpath Value[optValue] +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[optValue] +Path optValue +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +Hit: Expand catch-all + +TextDocumentEdit: CompletionExtensibleVariant.res +{ + "end": { "character": 3, "line": 64 }, + "start": { "character": 2, "line": 64 } +} +newText: + <--here + Some(Num(_) | Sub.Flag | Sub.Toggle(_) | Later | _) | None + +Xform src/CompletionExtensibleVariant.res start: 69:3, end: 69:8 +found selection: [69:3->69:8] -> [69:3->69:8] +XXX Not found! +Completable: Cpath Value[value] +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +ContextPath Value[value] +Path value +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 1 Stdlib +Hit: Exhaustive switch + +TextDocumentEdit: CompletionExtensibleVariant.res +{ + "end": { "character": 8, "line": 69 }, + "start": { "character": 3, "line": 69 } +} +newText: + <--here + switch value { + | Str(_) => %todo + | Num(_) => %todo + | Sub.Flag => %todo + | Sub.Toggle(_) => %todo + | Later => %todo + | _ => %todo + } + From 347aa661fd994cb2e1d7f7718b5c93b4f538f0fd Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Thu, 29 Oct 2026 18:03:36 +0000 Subject: [PATCH 10/10] Resolve an open's extension constructor only through its module Why: Codex review on #9085: an `open A` after a local extension `Value` resolved the payload of an unqualified `Value(...)` to the local constructor. Matching `A.Value` against the constructors also accepted the bare local `Value` as a suffix, and the later declaration won. A constructor named without a module is declared around the cursor, so an open no longer brings it into scope. Completion modified (extension payload after an open): Example: type attr = .. module A = { type attr += Value(bool) } type attr += Value(option) open A let x: attr = Value() Before (text): payload completion inside `Value()` completes option, the payload of the local Value After (text): payload completion inside `Value()` completes bool, the payload of A.Value: true, false Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo Signed-off-by: Christoph Knittel --- analysis/src/type_utils.ml | 5 +++- .../tests/src/CompletionExtensiblePayload.res | 9 ++++++ .../CompletionExtensiblePayload.res.txt | 29 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/analysis/src/type_utils.ml b/analysis/src/type_utils.ml index 22a71ff8e5..66e5de4670 100644 --- a/analysis/src/type_utils.ml +++ b/analysis/src/type_utils.ml @@ -426,7 +426,10 @@ let find_payload_constructor ~(env : Query_env.t) ~constructors ~extension_envs let opened = List.filteri (fun i _ -> i < List.length opened - 1) opened in - match last (named (opened @ path)) with + (* A constructor named without a module path is declared around + the cursor, so no open brings it into scope. *) + let in_module (c : Constructor.t) = List.length (parts c) > 1 in + match last (named (opened @ path) |> List.filter in_module) with | Some c -> Some c | None -> lookup scope) | _ :: scope -> lookup scope diff --git a/tests/analysis_tests/tests/src/CompletionExtensiblePayload.res b/tests/analysis_tests/tests/src/CompletionExtensiblePayload.res index 133b97961c..c2a1033160 100644 --- a/tests/analysis_tests/tests/src/CompletionExtensiblePayload.res +++ b/tests/analysis_tests/tests/src/CompletionExtensiblePayload.res @@ -120,3 +120,12 @@ module type S = { // switch v // ^com + +// An open after a local declaration shadows it in turn +open A + +// let x: attr = Value() +// ^com + +// switch v { | Value() } +// ^com diff --git a/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayload.res.txt b/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayload.res.txt index 9d6129f323..7308c6031b 100644 --- a/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayload.res.txt +++ b/tests/analysis_tests/tests/src/expected/CompletionExtensiblePayload.res.txt @@ -1254,3 +1254,32 @@ Path v } ] +Complete src/CompletionExtensiblePayload.res 126:23 +XXX Not found! +Completable: Cexpression Type[attr]->variantPayload::Value($0) +Raw opens: 3 A.place holder ... B.place holder ... A.place holder +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 4 Stdlib CompletionExtensiblePayload CompletionExtensiblePayload CompletionExtensiblePayload +ContextPath Type[attr] +Path attr +[ + { "detail": "bool", "kind": 4, "label": "true", "tags": [] }, + { "detail": "bool", "kind": 4, "label": "false", "tags": [] } +] + +Complete src/CompletionExtensiblePayload.res 129:22 +pos_cursor:[129:22] pos_no_white:[129:21] Found pattern:[129:16->129:23] +Ppat_construct Value:[129:16->129:21] +pos_cursor:[129:22] pos_no_white:[129:21] Found pattern:[129:21->129:23] +Ppat_construct ():[129:21->129:23] +Completable: Cpattern Value[v]->variantPayload::Value($0) +Raw opens: 3 A.place holder ... B.place holder ... A.place holder +Package opens Stdlib.place holder Pervasives.JsxModules.place holder +Resolved opens 4 Stdlib CompletionExtensiblePayload CompletionExtensiblePayload CompletionExtensiblePayload +ContextPath Value[v] +Path v +[ + { "detail": "bool", "kind": 4, "label": "true", "tags": [] }, + { "detail": "bool", "kind": 4, "label": "false", "tags": [] } +] +