Skip to content

Commit

Permalink
[flow][match] Propagate hints through match expressions case bodies
Browse files Browse the repository at this point in the history
Summary:
We need to propagate hints through match expressions case bodies.
The simple case is when we have some external hint. More interesting is getting hints from sibling case bodies. This can come out of order, and indeed this might be a common pattern with match expressions as people like to list their edge cases first. E.g.

```
declare const x: number;
const out = match (x) {
  0: [],
  const x: [x],
};
```
So, we consider every sibling case body, from top to bottom, as a "best effort hint".

The logic for hints diverges the match statement and match expression logic, so I break these up again into seperate methods.

Changelog: [internal]

Reviewed By: panagosg7

Differential Revision: D70140524

fbshipit-source-id: 499d5e8864964c32c6bce372743dec2adb4781cf
  • Loading branch information
gkz authored and facebook-github-bot committed Feb 26, 2025
1 parent b55e5c3 commit c313dc3
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 25 deletions.
75 changes: 50 additions & 25 deletions src/analysis/env_builder/name_def.ml
Original file line number Diff line number Diff line change
Expand Up @@ -2871,8 +2871,7 @@ class def_finder ~autocomplete_hooks ~react_jsx env_info toplevel_scope =
| Ast.Expression.Conditional expr -> this#visit_conditional ~hints expr
| Ast.Expression.AsConstExpression { Ast.Expression.AsConstExpression.expression; _ } ->
this#visit_expression ~hints ~cond expression
| Ast.Expression.Match x ->
this#visit_match ~on_case_body:(fun e -> ignore @@ this#expression e) x
| Ast.Expression.Match x -> this#visit_match_expression ~hints x
| Ast.Expression.Class _
| Ast.Expression.Identifier _
| Ast.Expression.Import _
Expand Down Expand Up @@ -3146,32 +3145,58 @@ class def_finder ~autocomplete_hooks ~react_jsx env_info toplevel_scope =

method! match_expression loc _ = fail loc "Should be visited by visit_match_expression"

method private visit_match_expression ~hints x =
let open Ast.Match in
let { arg; cases; match_keyword_loc; comments = _ } = x in
this#add_ordinary_binding
match_keyword_loc
(mk_reason RMatch match_keyword_loc)
(Binding (Root (Value { hints = []; expr = arg })));
let value_hints =
Base.List.foldi cases ~init:IMap.empty ~f:(fun i acc (_, { Case.body; _ }) ->
if expression_is_definitely_synthesizable ~autocomplete_hooks body then
let hint = Hint_t (ValueHint body, BestEffortHint) in
IMap.add i hint acc
else
acc
)
in
Base.List.iteri cases ~f:(fun i (case_loc, { Case.pattern; body; guard; comments = _ }) ->
let match_root =
(case_loc, Ast.Expression.Identifier (Flow_ast_utils.match_root_ident case_loc))
in
ignore @@ this#expression match_root;
let acc = Value { hints = []; expr = match_root } in
this#add_match_destructure_bindings acc pattern;
ignore @@ super#match_pattern pattern;
run_opt this#expression guard;
(* We use best-effort value hints for cases other than the current case.
Hints are ordered as the cases are in source, top to bottom. *)
let value_hints = value_hints |> IMap.remove i |> IMap.values |> List.rev in
let hints = Base.List.append hints value_hints in
this#visit_expression ~hints ~cond:NonConditionalContext body
)

method! match_statement _ x =
this#visit_match ~on_case_body:(fun b -> run_loc this#block b) x;
let open Ast.Match in
let { arg; cases; match_keyword_loc; comments = _ } = x in
this#add_ordinary_binding
match_keyword_loc
(mk_reason RMatch match_keyword_loc)
(Binding (Root (Value { hints = []; expr = arg })));
Base.List.iter cases ~f:(fun (case_loc, { Case.pattern; body; guard; comments = _ }) ->
let match_root =
(case_loc, Ast.Expression.Identifier (Flow_ast_utils.match_root_ident case_loc))
in
ignore @@ this#expression match_root;
let acc = Value { hints = []; expr = match_root } in
this#add_match_destructure_bindings acc pattern;
ignore @@ super#match_pattern pattern;
run_opt this#expression guard;
run_loc this#block body
);
x

method private visit_match
: 'B. on_case_body:('B -> unit) -> ('loc, 'loc, 'B) Ast.Match.t -> unit =
fun ~on_case_body x ->
let open Ast.Match in
let { arg; cases; match_keyword_loc; comments = _ } = x in
this#add_ordinary_binding
match_keyword_loc
(mk_reason RMatch match_keyword_loc)
(Binding (Root (Value { hints = []; expr = arg })));
Base.List.iter cases ~f:(function
| (case_loc, { Case.pattern; body; guard; comments = _ }) ->
let match_root =
(case_loc, Ast.Expression.Identifier (Flow_ast_utils.match_root_ident case_loc))
in
ignore @@ this#expression match_root;
let acc = Value { hints = []; expr = match_root } in
this#add_match_destructure_bindings acc pattern;
ignore @@ super#match_pattern pattern;
run_opt this#expression guard;
on_case_body body
)

method add_match_destructure_bindings root pattern =
let visit_binding loc name binding =
let binding = this#mk_hooklike_if_necessary (Flow_ast_utils.hook_name name) binding in
Expand Down
83 changes: 83 additions & 0 deletions tests/match/hints.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Annotation hint
type F = string => boolean;
{
declare const x: 'a';

const out: F = match (x) {
'a': y => true, // OK
};
}

// Sibling before
{
declare const x: 'a' | 'b';

const out = match (x) {
'a': [1],
'b': [], // Should be `Array<number>`
};
out as Array<number>; // OK
}

// Sibling after
{
declare const x: 'a' | 'b';

const out = match (x) {
'a': [], // Should be `Array<number>`
'b': [1],
};
out as Array<number>; // OK
}

// Multiple siblings, one valid
{
declare const x: 'a' | 'b' | 'c' | 'd';

const out = match (x) {
'a': 1,
'b': {},
'c': [1],
'd': [], // Should be `Array<number>`
};
out as number | {} | Array<number>; // OK
}

// Multiple siblings, multiple valid
{
declare const x: 'a' | 'b' | 'c';

const out = match (x) {
'a': [true],
'b': [1],
'c': [],
};
out as Array<boolean> | Array<number>; // OK
}
{
declare const x: 'a' | 'b' | 'c';

const out = match (x) {
'a': (x: number) => 1,
'b': (x: string) => true,
'c': x => x as number, // OK
};
}

// Cycles avoided
{
declare const x: 'a' | 'b';

const out: (x: number) => void = match (x) { // OK
'a': (x) => {},
'b': (x) => {},
};
}
{
declare const x: 'a' | 'b';

const out: Array<number> = match (x) { // OK
'a': [],
'b': [],
};
}

0 comments on commit c313dc3

Please sign in to comment.