Skip to content

Commit 6a38bea

Browse files
authored
destructure record rest elements (#8317)
* destructure record rest elements - fixes #8311 * support type with parameter for record rest * simplify parsing of record rest * update record spread error message * improve error message of superfluous fields in rest * improve error message of non optional rest field already matched * add a warning when rest record would be empty * add fixture tests for error/warning messages * add changelog * address comments (parsetree0 PPX roundtrips, nested rest, etc) * support rest of inline record * check rest field types, fix matching & invalid field identifier * fix rest of private type and analysis * use runtime field names for rest * support record type alias in rest * fix compiler crash when spreading the whole record * disallow rest spreading on packed modules * add tests for record rest with namespaced type * make sure rest is used and move logic to its own files * stop ignoring _rest in a few more places * format Signed-off-by: tsnobip <dontshootthink@gmail.com> * improve output (compile to JS destructuring) * update changelog * remove leading _ of used variable * add more tests * disallow spreading rest to unboxed record * address #8317 (comment) * document type_record_pat_rest function * add comment about Tpat_record rest * forbid destructure to rest of record with mutable fields * do not spread function params to avoid issues with 'use strict' * fix comment table for rest spread * Include record rest binders in rec check Signed-off-by: tsnobip <dontshootthink@gmail.com> * Track record rest types in dead analysis Signed-off-by: tsnobip <dontshootthink@gmail.com> * Rename record rest test config fixture Signed-off-by: tsnobip <dontshootthink@gmail.com> * add a warning for optional field overlap * no longer drop rest when printing pattern * make Parmatch.Conv.conv no longer drop rest * remove unused Object_rest_param * use internal attribute for ast mapper * improve completion support for record rest destructuring * simplify js dump shape * simplify materialize_fields * improve record rest completion Signed-off-by: tsnobip <dontshootthink@gmail.com> * print record rest in typedtree dumps Signed-off-by: tsnobip <dontshootthink@gmail.com> * simplify record rest implementation * restore better JS output for rest destructuring --------- Signed-off-by: tsnobip <dontshootthink@gmail.com>
1 parent 868bd08 commit 6a38bea

128 files changed

Lines changed: 3162 additions & 240 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
- Add a first-class `taggedTemplate<'param, 'output>` builtin type and the `TaggedTemplate` stdlib module (`TaggedTemplate.make`). Tagged-template tags are now tracked through the type system, so they emit real JS tagged-template syntax across module boundaries, when passed as first-class values, and when constructed at runtime by a factory (e.g. `postgres`). https://github.com/rescript-lang/rescript/pull/8461
2626
- Make mutation of private record mutable fields a configurable warning instead of a hard error. https://github.com/rescript-lang/rescript/pull/8366
27+
- Add support for pattern matching/destructuring of record rest. https://github.com/rescript-lang/rescript/pull/8317
2728

2829
#### :bug: Bug fix
2930

analysis/reanalyze/src/dead_value.ml

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,16 +230,78 @@ let rec collect_expr ~config ~refs ~file_deps ~cross_file
230230
With this annotation we declare a new type for each branch to allow the
231231
function to be typed.
232232
*)
233-
let collect_pattern ~config ~refs :
233+
let type_path_candidates ~file ~(module_path : Module_path.t) path =
234+
let path = Dce_path.from_path_t path in
235+
let module_context =
236+
module_path.path @ [File_context.module_name_tagged file]
237+
in
238+
let add_unique paths path =
239+
if List.exists (fun existing -> existing = path) paths then paths
240+
else path :: paths
241+
in
242+
[path; path @ module_context]
243+
|> List.fold_left
244+
(fun paths path ->
245+
[
246+
path;
247+
Dce_path.module_to_implementation path;
248+
Dce_path.module_to_interface path;
249+
]
250+
|> List.fold_left add_unique paths)
251+
[]
252+
253+
let add_record_label_type_references ~config ~refs ~pos_from labels =
254+
labels
255+
|> List.iter (fun {Types.ld_loc = {loc_start = pos_to; loc_ghost}; _} ->
256+
if not loc_ghost then
257+
Dead_type.add_type_reference ~config ~refs ~pos_from ~pos_to)
258+
259+
let add_record_rest_type_references_from_path ~config ~decls ~refs ~file
260+
~module_path ~pos_from rest =
261+
if !Config.analyze_types then
262+
match (Ctype.repr rest.Typedtree.rest_type).desc with
263+
| Types.Tconstr (path, _, _) ->
264+
let type_paths = type_path_candidates ~file ~module_path path in
265+
decls |> Declarations.builder_to_list
266+
|> List.iter (fun (_, decl) ->
267+
match (decl.Decl.decl_kind, decl.path) with
268+
| RecordLabel, _label :: type_path
269+
when List.exists
270+
(fun candidate -> candidate = type_path)
271+
type_paths ->
272+
Dead_type.add_type_reference ~config ~refs ~pos_from
273+
~pos_to:decl.pos
274+
| _ -> ())
275+
| _ -> ()
276+
277+
let add_record_rest_type_references ~config ~decls ~refs ~file ~module_path
278+
~pos_from ~env rest =
279+
if !Config.analyze_types then
280+
match
281+
try Some (Ctype.extract_concrete_typedecl env rest.Typedtree.rest_type)
282+
with Not_found -> None
283+
with
284+
| Some (_, _, {Types.type_kind = Type_record (labels, _)}) ->
285+
add_record_label_type_references ~config ~refs ~pos_from labels
286+
| _ ->
287+
add_record_rest_type_references_from_path ~config ~decls ~refs ~file
288+
~module_path ~pos_from rest
289+
290+
let collect_pattern ~config ~decls ~refs ~file ~module_path :
234291
_ -> _ -> Typedtree.pattern -> Typedtree.pattern =
235292
fun super self pat ->
236293
let pos_from = pat.Typedtree.pat_loc.loc_start in
237294
(match pat.pat_desc with
238-
| Typedtree.Tpat_record (cases, _clodsedFlag) ->
295+
| Typedtree.Tpat_record (cases, _clodsedFlag, rest) -> (
239296
cases
240297
|> List.iter (fun (_loc, {Types.lbl_loc = {loc_start = pos_to}}, _pat, _) ->
241298
if !Config.analyze_types then
242-
Dead_type.add_type_reference ~config ~refs ~pos_from ~pos_to)
299+
Dead_type.add_type_reference ~config ~refs ~pos_from ~pos_to);
300+
match rest with
301+
| None -> ()
302+
| Some rest ->
303+
add_record_rest_type_references ~config ~decls ~refs ~file ~module_path
304+
~pos_from:rest.rest_name.loc.loc_start ~env:pat.pat_env rest)
243305
| _ -> ());
244306
super.Tast_mapper.pat self pat
245307

@@ -331,7 +393,11 @@ let traverse_structure ~config ~decls ~refs ~file_deps ~cross_file ~file
331393
e
332394
|> collect_expr ~config ~refs ~file_deps ~cross_file ~last_binding
333395
super mapper);
334-
pat = (fun _self p -> p |> collect_pattern ~config ~refs super mapper);
396+
pat =
397+
(fun _self p ->
398+
p
399+
|> collect_pattern ~config ~decls ~refs ~file ~module_path super
400+
mapper);
335401
structure_item =
336402
(fun _self (structure_item : Typedtree.structure_item) ->
337403
let modulePath_for_item_opt =

analysis/src/completion_front_end.ml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -517,7 +517,7 @@ let completion_with_parser1 ~debug ~offset ~pos_cursor ~kind_file
517517
(NPolyvariantPayload {item_num = 0; constructor_name = txt}
518518
:: pattern_path)
519519
?context_path p
520-
| Ppat_record (fields, _) ->
520+
| Ppat_record (fields, _, rest) -> (
521521
Ext_list.iter fields (fun {lid = fname; x = p} ->
522522
match fname with
523523
| {Location.txt = Longident.Lident fname} ->
@@ -526,7 +526,16 @@ let completion_with_parser1 ~debug ~offset ~pos_cursor ~kind_file
526526
(Completable.NFollowRecordField {field_name = fname}
527527
:: pattern_path)
528528
?context_path p
529-
| _ -> ())
529+
| _ -> ());
530+
match rest with
531+
| None -> ()
532+
| Some {rest_name = {txt; loc}; rest_type; _} ->
533+
let context_path =
534+
match rest_type with
535+
| Some typ -> Type_utils.context_path_from_core_type typ
536+
| None -> context_path_to_save
537+
in
538+
scope := !scope |> Scope.add_value ~name:txt ~loc ?context_path)
530539
| Ppat_array pl ->
531540
pl
532541
|> List.iter

analysis/src/completion_patterns.ml

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ and traverse_pattern (pat : Parsetree.pattern) ~pattern_path ~loc_has_cursor
4848
Some v)
4949
else None
5050
in
51+
let rest_cursor (rest : Parsetree.record_pat_rest option) =
52+
match rest with
53+
| Some {rest_name = {txt; loc}; _} when loc_has_cursor loc ->
54+
Some (`Name txt)
55+
| Some {rest_loc; _} when loc_has_cursor rest_loc -> Some `Rest
56+
| _ -> None
57+
in
5158
match pat.ppat_desc with
5259
| Ppat_constant _ | Ppat_interval _ -> None
5360
| Ppat_constraint (p, _)
@@ -106,12 +113,16 @@ and traverse_pattern (pat : Parsetree.pattern) ~pattern_path ~loc_has_cursor
106113
[Completable.NTupleItem {item_num}] @ pattern_path)
107114
~result_from_found_item_num:(fun item_num ->
108115
[Completable.NTupleItem {item_num = item_num + 1}] @ pattern_path)
109-
| Ppat_record ([], _) ->
116+
| Ppat_record ([], _, rest) -> (
110117
(* Empty fields means we're in a record body `{}`. Complete for the fields. *)
111-
some_if_has_cursor
112-
("", [Completable.NRecordBody {seen_fields = []}] @ pattern_path)
113-
"Ppat_record(empty)"
114-
| Ppat_record (fields, _) -> (
118+
match rest_cursor rest with
119+
| Some (`Name txt) -> Some (txt, pattern_path)
120+
| Some `Rest -> None
121+
| None ->
122+
some_if_has_cursor
123+
("", [Completable.NRecordBody {seen_fields = []}] @ pattern_path)
124+
"Ppat_record(empty)")
125+
| Ppat_record (fields, _, rest) -> (
115126
let field_with_cursor = ref None in
116127
let field_with_pat_hole = ref None in
117128
Ext_list.iter fields (fun {lid = fname; x = f} ->
@@ -131,8 +142,10 @@ and traverse_pattern (pat : Parsetree.pattern) ~pattern_path ~loc_has_cursor
131142
| {Location.txt = Longident.Lident field_name} -> Some field_name
132143
| _ -> None)
133144
in
134-
match (!field_with_cursor, !field_with_pat_hole) with
135-
| Some (fname, f), _ | None, Some (fname, f) -> (
145+
match (rest_cursor rest, !field_with_cursor, !field_with_pat_hole) with
146+
| Some (`Name txt), _, _ -> Some (txt, pattern_path)
147+
| Some `Rest, _, _ -> None
148+
| None, Some (fname, f), _ | None, None, Some (fname, f) -> (
136149
match f.ppat_desc with
137150
| Ppat_extension ({txt = "rescript.patternhole"}, _) ->
138151
(* A pattern hole means for example `{someField: <com>}`. We want to complete for the type of `someField`. *)
@@ -154,7 +167,7 @@ and traverse_pattern (pat : Parsetree.pattern) ~pattern_path ~loc_has_cursor
154167
@ pattern_path)
155168
~loc_has_cursor ~first_char_before_cursor_no_white
156169
~pos_before_cursor)
157-
| None, None -> (
170+
| None, None, None -> (
158171
(* Figure out if we're completing for a new field.
159172
If the cursor is inside of the record body, but no field has the cursor,
160173
and there's no pattern hole. Check the first char to the left of the cursor,

analysis/src/dump_ast.ml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ let print_core_type typ ~pos =
6767
| Ptyp_variant _ -> "Ptyp_variant(<unimplemented>)"
6868
| _ -> "<unimplemented_ptyp_desc>"
6969

70+
let print_record_pattern_rest rest ~pos =
71+
(rest.Parsetree.rest_name |> print_loc_denominator_loc ~pos)
72+
^ rest.rest_name.txt
73+
^
74+
match rest.rest_type with
75+
| Some core_type -> " as " ^ print_core_type core_type ~pos
76+
| None -> ""
77+
7078
let rec print_pattern pattern ~pos ~indentation =
7179
print_attributes pattern.Parsetree.ppat_attributes
7280
^ (pattern.ppat_loc |> print_loc_denominator ~pos)
@@ -101,7 +109,7 @@ let rec print_pattern pattern ~pos ~indentation =
101109
| None -> ""
102110
| Some pat -> "," ^ print_pattern pat ~pos ~indentation)
103111
^ ")"
104-
| Ppat_record (fields, _) ->
112+
| Ppat_record (fields, _, rest) ->
105113
"Ppat_record(\n"
106114
^ add_indentation (indentation + 1)
107115
^ "fields:\n"
@@ -112,6 +120,14 @@ let rec print_pattern pattern ~pos ~indentation =
112120
^ ": "
113121
^ print_pattern pat ~pos ~indentation:(indentation + 2))
114122
|> String.concat "\n")
123+
^ (match rest with
124+
| None -> ""
125+
| Some rest ->
126+
"\n"
127+
^ add_indentation (indentation + 1)
128+
^ "rest:\n"
129+
^ add_indentation (indentation + 2)
130+
^ print_record_pattern_rest rest ~pos)
115131
^ "\n"
116132
^ add_indentation indentation
117133
^ ")"

analysis/src/hint.ml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,11 @@ let inlay ~source ~kind_file ~pos ~max_length ~full ~state ~debug =
4242
let rec process_pattern (pat : Parsetree.pattern) =
4343
match pat.ppat_desc with
4444
| Ppat_tuple pl -> pl |> List.iter process_pattern
45-
| Ppat_record (fields, _) ->
46-
Ext_list.iter fields (fun {x = p} -> process_pattern p)
45+
| Ppat_record (fields, _, rest) -> (
46+
Ext_list.iter fields (fun {x = p} -> process_pattern p);
47+
match rest with
48+
| Some {rest_name; _} -> push rest_name.loc Type
49+
| None -> ())
4750
| Ppat_array fields -> fields |> List.iter process_pattern
4851
| Ppat_var {loc} -> push loc Type
4952
| _ -> ()

analysis/src/process_cmt.ml

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -517,8 +517,27 @@ let rec for_structure_item ~(env : Shared_types.Env.t) ~(exported : Exported.t)
517517
| Tpat_tuple pats | Tpat_array pats | Tpat_construct (_, _, pats) ->
518518
pats |> List.iter (fun p -> handle_pattern [] p)
519519
| Tpat_or (p, _, _) -> handle_pattern [] p
520-
| Tpat_record (items, _) ->
521-
items |> List.iter (fun (_, _, p, _) -> handle_pattern [] p)
520+
| Tpat_record (record_items, _, rest) -> (
521+
record_items |> List.iter (fun (_, _, p, _) -> handle_pattern [] p);
522+
match rest with
523+
| None -> ()
524+
| Some rest ->
525+
let declared =
526+
add_declared ~name:rest.rest_name
527+
~stamp:(Ident.binding_time rest.rest_ident)
528+
~env ~extent:rest.rest_name.loc ~item:rest.rest_type []
529+
(Exported.add exported Exported.Value)
530+
Stamps.add_value
531+
in
532+
items :=
533+
{
534+
Module.kind = Module.Value declared.item;
535+
name = declared.name.txt;
536+
docstring = declared.docstring;
537+
deprecated = declared.deprecated;
538+
loc = declared.extent_loc;
539+
}
540+
:: !items)
522541
| Tpat_variant (_, Some p, _) -> handle_pattern [] p
523542
| Tpat_variant (_, None, _) | Tpat_any | Tpat_constant _ -> ()
524543
in

analysis/src/process_extra.ml

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -378,22 +378,32 @@ let pat ~(file : File.t) ~env ~extra (iter : Tast_iterator.iterator)
378378
| Tpackage (path, _, _) -> Some path
379379
| _ -> None
380380
in
381-
let add_for_pattern stamp name =
381+
let add_for_declared_pattern ~stamp ~name ~extent ~item ~attributes =
382382
if Stamps.find_value file.stamps stamp = None then (
383383
let declared =
384384
Process_attributes.new_declared ~name ~stamp ~module_path:NotVisible
385-
~extent:pattern.pat_loc ~item:pattern.pat_type false
386-
pattern.pat_attributes
385+
~extent ~item false attributes
387386
in
388387
Stamps.add_value file.stamps stamp declared;
389388
add_reference ~extra stamp name.loc;
390389
add_loc_item extra name.loc
391-
(Typed (name.txt, pattern.pat_type, Definition (stamp, Value))))
390+
(Typed (name.txt, item, Definition (stamp, Value))))
391+
in
392+
let add_for_pattern stamp name =
393+
add_for_declared_pattern ~stamp ~name ~extent:pattern.pat_loc
394+
~item:pattern.pat_type ~attributes:pattern.pat_attributes
392395
in
393396
(* Log.log("Entering pattern " ++ Utils.showLocation(pat_loc)); *)
394397
(match pattern.pat_desc with
395-
| Tpat_record (items, _) ->
396-
add_for_record ~env ~extra ~record_type:pattern.pat_type items
398+
| Tpat_record (items, _, rest) -> (
399+
add_for_record ~env ~extra ~record_type:pattern.pat_type items;
400+
match rest with
401+
| None -> ()
402+
| Some rest ->
403+
add_for_declared_pattern
404+
~stamp:(Ident.binding_time rest.rest_ident)
405+
~name:rest.rest_name ~extent:rest.rest_name.loc ~item:rest.rest_type
406+
~attributes:pattern.pat_attributes)
397407
| Tpat_construct (lident, constructor, _) ->
398408
add_for_constructor ~env ~extra pattern.pat_type lident constructor
399409
| Tpat_alias (_inner, ident, name) -> (

analysis/src/semantic_tokens.ml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,9 +233,13 @@ let command ~debug ~emitter ~source ~kind_file =
233233
| Ppat_construct ({txt = Lident ("true" | "false")}, _) ->
234234
(* Don't emit true or false *)
235235
Ast_iterator.default_iterator.pat iterator p
236-
| Ppat_record (cases, _) ->
236+
| Ppat_record (cases, _, rest) ->
237237
Ext_list.iter cases (fun {lid = label} ->
238238
emitter |> emit_record_label ~label ~debug);
239+
(match rest with
240+
| Some {rest_name = {txt = id; loc}; _} when is_lowercase_id id ->
241+
emitter |> emit_variable ~id ~debug ~loc
242+
| _ -> ());
239243
Ast_iterator.default_iterator.pat iterator p
240244
| Ppat_construct (name, _) ->
241245
emitter |> emit_variant ~name ~debug;
@@ -490,7 +494,7 @@ let command ~debug ~emitter ~source ~kind_file =
490494
in
491495
let {Res_driver.parsetree = structure; diagnostics} = parser ~source in
492496
if debug then
493-
Printf.printf "structure items:%d diagnostics:%d \n"
497+
Printf.printf "structure items:%d diagnostics:%d\n"
494498
(List.length structure) (List.length diagnostics);
495499
iterator.structure iterator structure |> ignore)
496500
else
@@ -499,7 +503,7 @@ let command ~debug ~emitter ~source ~kind_file =
499503
in
500504
let {Res_driver.parsetree = signature; diagnostics} = parser ~source in
501505
if debug then
502-
Printf.printf "signature items:%d diagnostics:%d \n"
506+
Printf.printf "signature items:%d diagnostics:%d\n"
503507
(List.length signature) (List.length diagnostics);
504508
iterator.signature iterator signature |> ignore
505509

analysis/src/signature_help.ml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,8 @@ let signature_help ~debug ~source ~kind_file ~pos
685685
match tuple_item_with_cursor with
686686
| None -> -1
687687
| Some i -> i)
688-
| `ConstructorPat (_, {ppat_desc = Ppat_record (fields, _)}) -> (
688+
| `ConstructorPat (_, {ppat_desc = Ppat_record (fields, _, _rest)})
689+
-> (
689690
let field_name_with_cursor =
690691
fields
691692
|> List.find_map

0 commit comments

Comments
 (0)