diff --git a/src/analysis/env_builder/name_def.ml b/src/analysis/env_builder/name_def.ml index fb331eda4ba..37aa7c6d47d 100644 --- a/src/analysis/env_builder/name_def.ml +++ b/src/analysis/env_builder/name_def.ml @@ -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 _ @@ -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 diff --git a/tests/match/hints.js b/tests/match/hints.js new file mode 100644 index 00000000000..9eb17ea3081 --- /dev/null +++ b/tests/match/hints.js @@ -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` + }; + out as Array; // OK +} + +// Sibling after +{ + declare const x: 'a' | 'b'; + + const out = match (x) { + 'a': [], // Should be `Array` + 'b': [1], + }; + out as Array; // 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` + }; + out as number | {} | Array; // OK +} + +// Multiple siblings, multiple valid +{ + declare const x: 'a' | 'b' | 'c'; + + const out = match (x) { + 'a': [true], + 'b': [1], + 'c': [], + }; + out as Array | Array; // 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 = match (x) { // OK + 'a': [], + 'b': [], + }; +}