Skip to content

Commit 462f92c

Browse files
cknittclaude
andcommitted
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kbf3gGkPma9LAZimuQjnrP Signed-off-by: Christoph Knittel <ck@cca.io>
1 parent 3cb8834 commit 462f92c

18 files changed

Lines changed: 1182 additions & 47 deletions

‎analysis/src/completion_back_end.ml‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2673,7 +2673,12 @@ let module_path_at ~(full : full) ~pos =
26732673

26742674
let process_completable ~state ~debug ~full ~scope ~env ~pos ~for_hover
26752675
completable =
2676+
(* A raw open ends in a placeholder for the name it qualifies. *)
2677+
let opens =
2678+
Scope.get_raw_opens scope
2679+
|> List.map (List.filter (fun name -> name <> "place holder"))
2680+
in
26762681
Type_utils.with_completion_scope ~file:full.file
2677-
~module_path:(module_path_at ~full ~pos) ~pos (fun () ->
2682+
~module_path:(module_path_at ~full ~pos) ~pos ~opens (fun () ->
26782683
process_completable_in_scope ~state ~debug ~full ~scope ~env ~pos
26792684
~for_hover completable)

‎analysis/src/completion_expressions.ml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ let rec traverse_expr (exp : Parsetree.expression) ~expr_path ~pos
157157
Completable.NVariantPayload
158158
{
159159
constructor_name = Longident.last txt;
160+
constructor_path = Longident.flatten txt;
160161
item_num = 0;
161162
source_arity = 1;
162163
};
@@ -170,6 +171,7 @@ let rec traverse_expr (exp : Parsetree.expression) ~expr_path ~pos
170171
Completable.NVariantPayload
171172
{
172173
constructor_name = Longident.last txt;
174+
constructor_path = Longident.flatten txt;
173175
item_num;
174176
source_arity = List.length args;
175177
};
@@ -180,6 +182,7 @@ let rec traverse_expr (exp : Parsetree.expression) ~expr_path ~pos
180182
Completable.NVariantPayload
181183
{
182184
constructor_name = Longident.last txt;
185+
constructor_path = Longident.flatten txt;
183186
item_num = item_num + 1;
184187
source_arity = List.length args;
185188
};

‎analysis/src/completion_front_end.ml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,7 @@ let completion_with_parser1 ~debug ~offset ~pos_cursor ~kind_file
504504
item_num = index;
505505
source_arity = List.length patterns;
506506
constructor_name = Longident.last txt;
507+
constructor_path = Longident.flatten txt;
507508
}
508509
:: pattern_path)
509510
?context_path)

‎analysis/src/completion_patterns.ml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ and traverse_pattern (pat : Parsetree.pattern) ~pattern_path ~loc_has_cursor
212212
Completable.NVariantPayload
213213
{
214214
constructor_name = Longident.last txt;
215+
constructor_path = Longident.flatten txt;
215216
item_num = 0;
216217
source_arity = 1;
217218
};
@@ -226,6 +227,7 @@ and traverse_pattern (pat : Parsetree.pattern) ~pattern_path ~loc_has_cursor
226227
Completable.NVariantPayload
227228
{
228229
constructor_name = Longident.last txt;
230+
constructor_path = Longident.flatten txt;
229231
item_num;
230232
source_arity = List.length patterns;
231233
};
@@ -236,6 +238,7 @@ and traverse_pattern (pat : Parsetree.pattern) ~pattern_path ~loc_has_cursor
236238
Completable.NVariantPayload
237239
{
238240
constructor_name = Longident.last txt;
241+
constructor_path = Longident.flatten txt;
239242
item_num = item_num + 1;
240243
source_arity = List.length patterns;
241244
};

‎analysis/src/shared_types.ml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,9 @@ and completion_type =
378378
| Tvariant of {
379379
env: Query_env.t;
380380
constructors: Constructor.t list;
381+
extension_envs: (Constructor.t * Query_env.t) list;
382+
(** The environments of the files declaring extension
383+
constructors, where their payloads are typed *)
381384
variant_decl: Types.type_declaration;
382385
variant_name: string;
383386
}
@@ -606,6 +609,8 @@ module Completable = struct
606609
| NRecordBody of {seen_fields: string list}
607610
| NVariantPayload of {
608611
constructor_name: string;
612+
constructor_path: string list;
613+
(** The constructor as written, with its module path *)
609614
item_num: int;
610615
source_arity: int;
611616
}

‎analysis/src/type_utils.ml‎

Lines changed: 120 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -361,27 +361,87 @@ let extract_function_type2 ?type_arg_context ~state ~env ~package typ =
361361
in
362362
loop ?type_arg_context ~env [] typ
363363

364-
(* The file being edited and the module path at the cursor, while a
365-
completion request runs. Extension constructors are named relative to
366-
it: unlike ordinary constructors they can't be found from the expected
367-
type, so they have to be written so that they are in scope there. *)
368-
let completion_scope : (File.t * string list * Pos.t) option ref = ref None
369-
370-
let with_completion_scope ~file ~module_path ~pos f =
364+
(* The file being edited, the module path at the cursor and the modules
365+
opened there, most recent first, while a completion request runs.
366+
Extension constructors are named relative to it: unlike ordinary
367+
constructors they can't be found from the expected type, so they have to
368+
be written so that they are in scope there. *)
369+
let completion_scope :
370+
(File.t * string list * Pos.t * string list list) option ref =
371+
ref None
372+
373+
let with_completion_scope ~file ~module_path ~pos ~opens f =
371374
let previous = !completion_scope in
372-
completion_scope := Some (file, module_path, pos);
375+
completion_scope := Some (file, module_path, pos, opens);
373376
Fun.protect ~finally:(fun () -> completion_scope := previous) f
374377

375-
(* Whether [c] is the constructor written [name] in a payload position, where
376-
only the last part of a path such as [Sub.Flag] is kept. Extension
377-
constructors are named by their module path, ordinary ones are not. *)
378-
let constructor_has_name (c : Constructor.t) name =
379-
c.cname.txt = name
380-
||
381-
match String.rindex_opt c.cname.txt '.' with
382-
| Some i ->
383-
String.sub c.cname.txt (i + 1) (String.length c.cname.txt - i - 1) = name
384-
| None -> false
378+
(* The constructor written [path] in a payload position, with the
379+
environment its payload is typed in. Extension constructors are named by
380+
their module path, so the one sharing the longest suffix with [path] is
381+
meant. Of several equally good ones, the one that came into scope last
382+
wins: through the most recent open naming its unwritten qualifier, else
383+
with nothing left unwritten, the last declared shadowing the others. *)
384+
let find_payload_constructor ~env ~constructors ~extension_envs path =
385+
let opens =
386+
match !completion_scope with
387+
| Some (_, _, _, opens) -> opens
388+
| None -> []
389+
in
390+
let rec common_suffix a b =
391+
match (a, b) with
392+
| x :: a, y :: b when x = y -> 1 + common_suffix a b
393+
| _ -> 0
394+
in
395+
let rec ends_with l suffix =
396+
l = suffix
397+
||
398+
match l with
399+
| _ :: l -> ends_with l suffix
400+
| [] -> false
401+
in
402+
let written = List.rev path in
403+
let scored =
404+
constructors
405+
|> List.map (fun (c : Constructor.t) ->
406+
let parts = List.rev (String.split_on_char '.' c.cname.txt) in
407+
let score = common_suffix parts written in
408+
let qualifier = List.rev (List.filteri (fun i _ -> i >= score) parts) in
409+
(c, score, qualifier))
410+
in
411+
let best = List.fold_left (fun m (_, score, _) -> max m score) 0 scored in
412+
let candidates =
413+
scored |> List.filter (fun (_, score, _) -> best > 0 && score = best)
414+
in
415+
(* How recently the constructor came into scope: the position in [opens]
416+
of the module its unwritten qualifier names, or after all opens when
417+
nothing is left unwritten. *)
418+
let scope_rank qualifier =
419+
let rec find i = function
420+
| [] -> None
421+
| o :: opens ->
422+
if ends_with qualifier o || ends_with o qualifier then Some i
423+
else find (i + 1) opens
424+
in
425+
if qualifier = [] then Some (List.length opens) else find 0 opens
426+
in
427+
let in_scope =
428+
candidates
429+
|> List.fold_left
430+
(fun found (c, _, qualifier) ->
431+
match (found, scope_rank qualifier) with
432+
| Some (best, _), Some rank when rank <= best -> Some (rank, c)
433+
| None, Some rank -> Some (rank, c)
434+
| found, _ -> found)
435+
None
436+
in
437+
let found =
438+
match (in_scope, List.rev candidates) with
439+
| Some (_, c), _ | None, (c, _, _) :: _ -> Some c
440+
| None, [] -> None
441+
in
442+
found
443+
|> Option.map (fun c ->
444+
(c, List.assq_opt c extension_envs |> Option.value ~default:env))
385445

386446
(* The stamp under which [typ] is declared in [file]. *)
387447
let type_stamp (file : File.t) (typ : Type.t) =
@@ -396,12 +456,13 @@ let type_stamp (file : File.t) (typ : Type.t) =
396456
type it extends by its path where it is declared: an identifier of the
397457
type's own file, or a path resolved from the extension's file. The
398458
constructors are named by their module path, since an extension
399-
constructor can't be found from the expected type alone. *)
459+
constructor can't be found from the expected type alone, and come with
460+
the environment of their file, where their payloads are typed. *)
400461
let extension_constructors ~state ~package ~(current_file : File.t)
401462
~(type_file : File.t) ~type_stamp ~type_name ~decl =
402463
let current_file, scope_path, cursor =
403464
match !completion_scope with
404-
| Some (file, module_path, pos) -> (file, module_path, Some pos)
465+
| Some (file, module_path, pos, _) -> (file, module_path, Some pos)
405466
| None -> (current_file, [], None)
406467
in
407468
(* 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)
470531
if file.uri = current_file.uri then relative path scope_path
471532
else Utils.flatten_any_namespace_in_path (file.module_name :: path)
472533
in
473-
{
474-
declared.item with
475-
cname = {declared.item.cname with txt = String.concat "." path};
476-
type_decl = (type_name, decl);
477-
})
534+
( {
535+
declared.item with
536+
cname = {declared.item.cname with txt = String.concat "." path};
537+
type_decl = (type_name, decl);
538+
},
539+
Query_env.from_file file ))
478540

479541
let rec extract_type ?(print_opening_debug = true)
480542
?(type_arg_context : type_arg_context option)
@@ -556,6 +618,7 @@ let rec extract_type ?(print_opening_debug = true)
556618
{
557619
env = env_from_item;
558620
constructors;
621+
extension_envs = [];
559622
variant_name = name.txt;
560623
variant_decl = decl;
561624
},
@@ -573,12 +636,13 @@ let rec extract_type ?(print_opening_debug = true)
573636
| [] when name.txt = "t" ->
574637
Some (TtypeT {env = env_from_item; path}, type_arg_context)
575638
| [] -> None
576-
| constructors ->
639+
| extension_envs ->
577640
Some
578641
( Tvariant
579642
{
580643
env = env_from_item;
581-
constructors;
644+
constructors = List.map fst extension_envs;
645+
extension_envs;
582646
variant_name = name.txt;
583647
variant_decl = decl;
584648
},
@@ -738,19 +802,30 @@ let extract_type_from_resolved_type (typ : Type.t) ~env ~full ~state =
738802
| Variant constructors ->
739803
Some
740804
(Tvariant
741-
{env; constructors; variant_name = typ.name; variant_decl = typ.decl})
805+
{
806+
env;
807+
constructors;
808+
extension_envs = [];
809+
variant_name = typ.name;
810+
variant_decl = typ.decl;
811+
})
742812
| Open when typ.decl.type_manifest = None -> (
743813
match
744814
extension_constructors ~state ~package:full.package
745815
~current_file:full.file ~type_file:env.file
746816
~type_stamp:(type_stamp env.file typ) ~type_name:typ.name ~decl:typ.decl
747817
with
748818
| [] -> None
749-
| constructors ->
819+
| extension_envs ->
750820
Some
751821
(Tvariant
752-
{env; constructors; variant_name = typ.name; variant_decl = typ.decl})
753-
)
822+
{
823+
env;
824+
constructors = List.map fst extension_envs;
825+
extension_envs;
826+
variant_name = typ.name;
827+
variant_decl = typ.decl;
828+
}))
754829
| Abstract _ | Open -> (
755830
match typ.decl.type_manifest with
756831
| None -> None
@@ -910,19 +985,19 @@ let rec resolve_nested ?type_arg_context ~env ~full ~state ~nested ?ctx
910985
|> extract_type ~env ~state ~package:full.package
911986
|> Utils.Option.flat_map (fun (t, type_arg_context) ->
912987
t |> resolve_nested ?type_arg_context ~env ~state ~full ~nested)
913-
| ( NVariantPayload {constructor_name; item_num; source_arity},
914-
Tvariant {env; constructors} ) -> (
988+
| ( NVariantPayload
989+
{constructor_name; constructor_path; item_num; source_arity},
990+
Tvariant {env; constructors; extension_envs} ) -> (
915991
if Debug.verbose () then
916992
Printf.printf
917993
"[nested]--> trying to move into variant payload $%i of constructor \
918994
'%s'\n"
919995
item_num constructor_name;
920996
match
921-
constructors
922-
|> List.find_opt (fun (c : Constructor.t) ->
923-
constructor_has_name c constructor_name)
997+
find_payload_constructor ~env ~constructors ~extension_envs
998+
constructor_path
924999
with
925-
| Some {args = Args args} -> (
1000+
| Some ({args = Args args}, env) -> (
9261001
if Debug.verbose () then
9271002
print_endline "[nested]--> found constructor (Args type)";
9281003
let item_num, nested =
@@ -949,7 +1024,7 @@ let rec resolve_nested ?type_arg_context ~env ~full ~state ~nested ?ctx
9491024
(List.length nested);
9501025
typ |> resolve_nested ?type_arg_context ~env ~state ~full ~nested)
9511026
)
952-
| Some {args = InlineRecord fields} when item_num = 0 ->
1027+
| Some ({args = InlineRecord fields}, env) when item_num = 0 ->
9531028
if Debug.verbose () then
9541029
print_endline "[nested]--> found constructor (inline record)";
9551030
TinlineRecord {env; fields}
@@ -1051,14 +1126,13 @@ let rec resolve_nested_pattern_path (typ : inner_type) ~env ~full ~state ~nested
10511126
| None -> None
10521127
| Some typ ->
10531128
TypeExpr typ |> resolve_nested_pattern_path ~env ~state ~full ~nested)
1054-
| ( NVariantPayload {constructor_name; item_num; source_arity},
1055-
Tvariant {env; constructors} ) -> (
1129+
| ( NVariantPayload {constructor_path; item_num; source_arity},
1130+
Tvariant {env; constructors; extension_envs} ) -> (
10561131
match
1057-
constructors
1058-
|> List.find_opt (fun (constructor : Constructor.t) ->
1059-
constructor_has_name constructor constructor_name)
1132+
find_payload_constructor ~env ~constructors ~extension_envs
1133+
constructor_path
10601134
with
1061-
| Some {args = Args args} -> (
1135+
| Some ({args = Args args}, env) -> (
10621136
let item_num, nested =
10631137
normalize_constructor_payload_path
10641138
~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
10681142
TypeExpr typ
10691143
|> resolve_nested_pattern_path ~env ~state ~full ~nested
10701144
| None -> None)
1071-
| Some {args = InlineRecord fields} when item_num = 0 ->
1145+
| Some ({args = InlineRecord fields}, env) when item_num = 0 ->
10721146
ExtractedType (TinlineRecord {env; fields})
10731147
|> resolve_nested_pattern_path ~env ~state ~full ~nested
1074-
| Some {args = InlineRecord _} | None -> None)
1148+
| Some ({args = InlineRecord _}, _) | None -> None)
10751149
| ( NPolyvariantPayload {constructor_name; item_num; source_arity},
10761150
Tpolyvariant {env; constructors} ) -> (
10771151
match

0 commit comments

Comments
 (0)