From 9dc295be6ff53dc596e5e449d0753237f167f2f5 Mon Sep 17 00:00:00 2001 From: George Zahariev Date: Tue, 17 Dec 2024 15:02:00 -0800 Subject: [PATCH] [flow][match] Match array patterns apply refinements Summary: Match array patterns apply refinements to the match argument. For this purpose, we add a new `ArrLen` predicate. It can handle both `=== length` and `>= length` Changelog: [internal] Reviewed By: SamChou19815 Differential Revision: D67261333 fbshipit-source-id: e7823f5923a37b64ada89fd9659ec6d68f86dd54 --- .../__tests__/env_builder_refinement_test.ml | 27 ++++ src/analysis/env_builder/env_api.ml | 25 ++++ src/analysis/env_builder/name_def_ordering.ml | 1 + src/analysis/env_builder/name_resolver.ml | 48 ++++++- src/typing/predicate_kit.ml | 11 ++ src/typing/type.ml | 15 +++ src/typing/typeUtil.ml | 1 + src/typing/type_env.ml | 7 + src/typing/type_filter.ml | 38 ++++++ src/typing/type_filter.mli | 2 + src/typing/type_mapper.ml | 1 + src/typing/type_visitor.ml | 1 + tests/match/match.exp | 121 ++++++++++++++++- tests/match/matching.js | 123 ++++++++++++++++++ 14 files changed, 417 insertions(+), 4 deletions(-) diff --git a/src/analysis/env_builder/__tests__/env_builder_refinement_test.ml b/src/analysis/env_builder/__tests__/env_builder_refinement_test.ml index 6014d022fcb..5d3a318f76c 100644 --- a/src/analysis/env_builder/__tests__/env_builder_refinement_test.ml +++ b/src/analysis/env_builder/__tests__/env_builder_refinement_test.ml @@ -7507,3 +7507,30 @@ let%expect_test "match_object_pattern" = {refinement = And (And (object, Not (Null)), SentinelR type); writes = {refinement = Or (Or (Not (And (object, Not (Null))), Not (SentinelR type)), Not (PropExistsR (value))); writes = (2, 1) to (2, 6): (``)}} }] |}] + +let%expect_test "match_array_pattern" = + (* Test case aligned with object pattern test case above. *) + print_ssa_test {| +(match (x) { + [ 'foo', const a]: a as number, + [ 'bar']: 1, +}); +|}; + [%expect {| + [ + (2, 1) to (2, 6) => { + (2, 1) to (2, 6): (``) + }; + (2, 8) to (2, 9) => { + Global x + }; + (3, 2) to (3, 45) => { + {refinement = And (And (isArray, array length === 2), SentinelR 0); writes = (2, 1) to (2, 6): (``)} + }; + (3, 33) to (3, 34) => { + (3, 29) to (3, 30): (`a`) + }; + (4, 2) to (4, 19) => { + {refinement = And (And (isArray, array length === 1), SentinelR 0); writes = {refinement = Or (Not (And (isArray, array length === 2)), Not (SentinelR 0)); writes = (2, 1) to (2, 6): (``)}} + }] + |}] diff --git a/src/analysis/env_builder/env_api.ml b/src/analysis/env_builder/env_api.ml index 91b5a4d3c28..5cf03a58900 100644 --- a/src/analysis/env_builder/env_api.ml +++ b/src/analysis/env_builder/env_api.ml @@ -114,6 +114,11 @@ module type S = sig type values = read L.LMap.t module Refi : sig + type array_length_op = + | ArrLenEqual + | ArrLenGreaterThanEqual + [@@deriving show] + type refinement_kind = | AndR of refinement_kind * refinement_kind | OrR of refinement_kind * refinement_kind @@ -124,6 +129,10 @@ module type S = sig | MaybeR | InstanceOfR of (L.t, L.t) Ast.Expression.t | IsArrayR + | ArrLenR of { + op: array_length_op; + n: int; + } | BoolR of L.t | FunctionR | NumberR of L.t @@ -407,6 +416,11 @@ module Make and write_locs = write_loc list module Refi = struct + type array_length_op = + | ArrLenEqual + | ArrLenGreaterThanEqual + [@@deriving show] + type refinement_kind = | AndR of refinement_kind * refinement_kind | OrR of refinement_kind * refinement_kind @@ -417,6 +431,10 @@ module Make | MaybeR | InstanceOfR of (L.t, L.t) Ast.Expression.t | IsArrayR + | ArrLenR of { + op: array_length_op; + n: int; + } | BoolR of L.t | FunctionR | NumberR of L.t @@ -671,6 +689,13 @@ module Make | MaybeR -> "Maybe" | InstanceOfR _ -> "instanceof" | IsArrayR -> "isArray" + | ArrLenR { op; n } -> + let op = + match op with + | ArrLenEqual -> "===" + | ArrLenGreaterThanEqual -> ">=" + in + Printf.sprintf "array length %s %i" op n | BoolR _ -> "bool" | FunctionR -> "function" | NumberR _ -> "number" diff --git a/src/analysis/env_builder/name_def_ordering.ml b/src/analysis/env_builder/name_def_ordering.ml index 985903ddac1..83bf41c005e 100644 --- a/src/analysis/env_builder/name_def_ordering.ml +++ b/src/analysis/env_builder/name_def_ordering.ml @@ -223,6 +223,7 @@ struct | UndefinedR | MaybeR | IsArrayR + | ArrLenR _ | BoolR _ | FunctionR | NumberR _ diff --git a/src/analysis/env_builder/name_resolver.ml b/src/analysis/env_builder/name_resolver.ml index 001fd4b901a..7119a7e86ae 100644 --- a/src/analysis/env_builder/name_resolver.ml +++ b/src/analysis/env_builder/name_resolver.ml @@ -3170,9 +3170,51 @@ module Make (Context : C) (FlowAPIUtils : F with type cx = Context.t) : | _ -> ()); recurse member pattern ) - | (_, ArrayPattern _) -> - (* TODO:match *) - () + | (loc, ArrayPattern { ArrayPattern.elements; rest; comments = _ }) -> + (match RefinementKey.of_expression acc with + | Some key -> + let refis = + this#start_refinement key ~refining_locs:(L.LSet.singleton loc) IsArrayR + in + (* Check the length *) + let refis = + this#extend_refinement + key + ~refining_locs:(L.LSet.singleton loc) + (ArrLenR + { + op = + ( if Base.Option.is_some rest then + ArrLenGreaterThanEqual + else + ArrLenEqual + ); + n = Base.List.length elements; + } + ) + refis + in + this#commit_refinement refis + | None -> ()); + let number_of_i i = + Ast.Expression.NumberLiteral + { Ast.NumberLiteral.value = float i; raw = string_of_int i; comments = None } + in + Base.List.iteri elements ~f:(fun i { ArrayPattern.Element.pattern; index } -> + let (pat_loc, _) = pattern in + let member = + ( index, + let open Ast.Expression in + Member + { + Member._object = acc; + property = Member.PropertyExpression (pat_loc, number_of_i i); + comments = None; + } + ) + in + recurse member pattern + ) in recurse arg root_pattern diff --git a/src/typing/predicate_kit.ml b/src/typing/predicate_kit.ml index ddc33b499c2..bc75e69a0d3 100644 --- a/src/typing/predicate_kit.ml +++ b/src/typing/predicate_kit.ml @@ -233,6 +233,17 @@ and predicate_no_concretization cx trace result_collector l ~p = | ArrP -> report_filtering_result_to_predicate_result (Type_filter.array l) result_collector | NotP ArrP -> report_filtering_result_to_predicate_result (Type_filter.not_array l) result_collector + (*******************) + (* array length *) + (*******************) + | ArrLenP { op; n } -> + report_filtering_result_to_predicate_result + (Type_filter.array_length ~sense:true ~op ~n l) + result_collector + | NotP (ArrLenP { op; n }) -> + report_filtering_result_to_predicate_result + (Type_filter.array_length ~sense:false ~op ~n l) + result_collector (***********************) (* typeof _ ~ "undefined" *) (***********************) diff --git a/src/typing/type.ml b/src/typing/type.ml index cc317395571..0aaf438d1b2 100644 --- a/src/typing/type.ml +++ b/src/typing/type.ml @@ -1014,6 +1014,10 @@ module rec TypeTerm : sig | SymbolP of ALoc.t (* symbol *) | VoidP (* undefined *) | ArrP (* Array.isArray *) + | ArrLenP of { + op: array_length_op; + n: int; + } (* `if ('b' in a)` yields `flow (a, PredicateT(PropExistsP ("b"), tout))` *) | PropExistsP of { propname: string; @@ -1049,6 +1053,10 @@ module rec TypeTerm : sig (* e1 === e2 *) | EqTest + and array_length_op = + | ArrLenEqual + | ArrLenGreaterThanEqual + and literal = | Truthy | AnyLiteral @@ -4291,6 +4299,13 @@ let rec string_of_predicate = function | SymbolP _ -> "symbol" (* Array.isArray *) | ArrP -> "array" + | ArrLenP { op; n } -> + let op = + match op with + | ArrLenEqual -> "===" + | ArrLenGreaterThanEqual -> ">=" + in + spf "array length %s %i" op n | PropExistsP { propname; _ } -> spf "prop `%s` exists" propname | PropTruthyP (key, _) -> spf "prop `%s` is truthy" key | PropIsExactlyNullP (key, _) -> spf "prop `%s` is exactly null" key diff --git a/src/typing/typeUtil.ml b/src/typing/typeUtil.ml index a937ce36479..f7e88df2339 100644 --- a/src/typing/typeUtil.ml +++ b/src/typing/typeUtil.ml @@ -759,6 +759,7 @@ let rec eq_predicate (p1, p2) = | (PropExistsP { propname = s1; _ }, PropExistsP { propname = s2; _ }) -> s1 = s2 | (PropTruthyP (s1, _), PropTruthyP (s2, _)) -> s1 = s2 | (PropNonMaybeP (s1, _), PropNonMaybeP (s2, _)) -> s1 = s2 + | (ArrLenP { op = op1; n = n1 }, ArrLenP { op = op2; n = n2 }) -> op1 = op2 && n1 = n2 (* Complex *) | (BinaryP (b1, OpenT (_, id1)), BinaryP (b2, OpenT (_, id2))) -> b1 = b2 && id1 = id2 | (BinaryP _, BinaryP _) -> p1 = p2 diff --git a/src/typing/type_env.ml b/src/typing/type_env.ml index 84614b7d131..e43f4d12c7e 100644 --- a/src/typing/type_env.ml +++ b/src/typing/type_env.ml @@ -308,6 +308,13 @@ let predicate_of_refinement cx = Type_operation_utils.TypeAssertions.assert_instanceof_rhs cx t; Some (BinaryP (InstanceofTest, t)) | IsArrayR -> Some ArrP + | ArrLenR { op; n } -> + let op = + match op with + | Env_api.Refi.ArrLenEqual -> Type.ArrLenEqual + | Env_api.Refi.ArrLenGreaterThanEqual -> Type.ArrLenGreaterThanEqual + in + Some (ArrLenP { op; n }) | BoolR loc -> Some (BoolP loc) | FunctionR -> Some FunP | NumberR loc -> Some (NumP loc) diff --git a/src/typing/type_filter.ml b/src/typing/type_filter.ml index 2d75fcc6cad..3fcffa773c9 100644 --- a/src/typing/type_filter.ml +++ b/src/typing/type_filter.ml @@ -597,6 +597,44 @@ let not_array t = | DefT (_, ArrT _) -> DefT (reason_of_t t, EmptyT) |> changed_result | _ -> unchanged_result t +let array_length ~sense ~op ~n t = + match t with + | DefT (_, ArrT (TupleAT { arity = (num_req, num_total); inexact; _ })) -> + (* `None` represents "maybe" a match *) + let matches = + match op with + | ArrLenEqual -> + if n = num_req && n = num_total && not inexact then + Some true + else if n >= num_req && (n <= num_total || inexact) then + None + else + Some false + | ArrLenGreaterThanEqual -> + if n <= num_req then + Some true + else if n <= num_total || inexact then + None + else + Some false + in + (match (matches, sense) with + | (Some true, true) + | (Some false, false) + | (None, _) -> + unchanged_result t + | (Some false, true) + | (Some true, false) -> + DefT (reason_of_t t, EmptyT) |> changed_result) + | DefT (_, ArrT (ArrayAT _ | ROArrayAT _)) -> + (* `[...]` matches every length, so arrays are matched. *) + let matches = n = 0 && op = ArrLenGreaterThanEqual in + if matches = sense then + unchanged_result t + else + DefT (reason_of_t t, EmptyT) |> changed_result + | _ -> unchanged_result t + let sentinel_refinement = let open UnionEnum in let enum_match sense = function diff --git a/src/typing/type_filter.mli b/src/typing/type_filter.mli index 164293478c5..35d5a02058e 100644 --- a/src/typing/type_filter.mli +++ b/src/typing/type_filter.mli @@ -83,6 +83,8 @@ val array : Type.t -> filter_result val not_array : Type.t -> filter_result +val array_length : sense:bool -> op:Type.array_length_op -> n:int -> Type.t -> filter_result + val sentinel_refinement : Type.t -> Reason.t -> Type.t -> bool -> Type.UnionEnum.star -> filter_result diff --git a/src/typing/type_mapper.ml b/src/typing/type_mapper.ml index d9c4cde9b8f..c27fd1737b0 100644 --- a/src/typing/type_mapper.ml +++ b/src/typing/type_mapper.ml @@ -912,6 +912,7 @@ class virtual ['a] t = | SymbolP _ | VoidP | ArrP + | ArrLenP _ | PropNonMaybeP _ | PropNonVoidP _ | PropIsExactlyNullP _ diff --git a/src/typing/type_visitor.ml b/src/typing/type_visitor.ml index 18c44bc4121..d5fcaf09808 100644 --- a/src/typing/type_visitor.ml +++ b/src/typing/type_visitor.ml @@ -189,6 +189,7 @@ class ['a] t = | SymbolP _ -> acc | VoidP -> acc | ArrP -> acc + | ArrLenP _ -> acc | PropTruthyP _ -> acc | PropExistsP _ -> acc | PropNonVoidP _ -> acc diff --git a/tests/match/match.exp b/tests/match/match.exp index 5146d4eb3c1..a9fe5c8fe68 100644 --- a/tests/match/match.exp +++ b/tests/match/match.exp @@ -312,6 +312,125 @@ References: ^^^^^ [2] +Error ----------------------------------------------------------------------------------------------- matching.js:302:14 + +Cannot cast `d` to empty because tuple type [1] is incompatible with empty [2]. [incompatible-cast] + + matching.js:302:14 + 302| const d: d as empty, // ERROR: `'baz'` element not checked + ^ + +References: + matching.js:290:20 + 290| | ['baz', boolean]; + ^^^^^^^^^^^^^^^^ [1] + matching.js:302:19 + 302| const d: d as empty, // ERROR: `'baz'` element not checked + ^^^^^ [2] + + +Error ----------------------------------------------------------------------------------------------- matching.js:338:22 + +Cannot cast `a` to empty because boolean [1] is incompatible with empty [2]. [incompatible-cast] + + matching.js:338:22 + 338| [const a, _, _]: a as empty, // ERROR: `boolean` is not `empty` + ^ + +References: + matching.js:333:21 + 333| | [boolean, boolean, boolean]; + ^^^^^^^ [1] + matching.js:338:27 + 338| [const a, _, _]: a as empty, // ERROR: `boolean` is not `empty` + ^^^^^ [2] + + +Error ----------------------------------------------------------------------------------------------- matching.js:358:16 + +Cannot cast `a` to string because number [1] is incompatible with string [2]. [incompatible-cast] + + matching.js:358:16 + 358| [const a]: a as string, // ERROR: `number` is not `string` + ^ + +References: + matching.js:354:21 + 354| declare const x: [number] | Array; + ^^^^^^ [1] + matching.js:358:21 + 358| [const a]: a as string, // ERROR: `number` is not `string` + ^^^^^^ [2] + + +Error ----------------------------------------------------------------------------------------------- matching.js:380:14 + +Cannot cast `d` to empty because tuple type [1] is incompatible with empty [2]. [incompatible-cast] + + matching.js:380:14 + 380| const d: d as empty, // ERROR: does not match all possibilities + ^ + +References: + matching.js:371:20 + 371| declare const x: [a: 0, b?: 1, c?: 2]; + ^^^^^^^^^^^^^^^^^^^^ [1] + matching.js:380:19 + 380| const d: d as empty, // ERROR: does not match all possibilities + ^^^^^ [2] + + +Error ----------------------------------------------------------------------------------------------- matching.js:385:14 + +Cannot cast `d` to empty because tuple type [1] is incompatible with empty [2]. [incompatible-cast] + + matching.js:385:14 + 385| const d: d as empty, // ERROR: does not match all possibilities + ^ + +References: + matching.js:371:20 + 371| declare const x: [a: 0, b?: 1, c?: 2]; + ^^^^^^^^^^^^^^^^^^^^ [1] + matching.js:385:19 + 385| const d: d as empty, // ERROR: does not match all possibilities + ^^^^^ [2] + + +Error ----------------------------------------------------------------------------------------------- matching.js:390:14 + +Cannot cast `d` to empty because tuple type [1] is incompatible with empty [2]. [incompatible-cast] + + matching.js:390:14 + 390| const d: d as empty, // ERROR: does not match all possibilities + ^ + +References: + matching.js:371:20 + 371| declare const x: [a: 0, b?: 1, c?: 2]; + ^^^^^^^^^^^^^^^^^^^^ [1] + matching.js:390:19 + 390| const d: d as empty, // ERROR: does not match all possibilities + ^^^^^ [2] + + +Error ----------------------------------------------------------------------------------------------- matching.js:405:14 + +Cannot cast `d` to empty because tuple type [1] is incompatible with empty [2]. [incompatible-cast] + + matching.js:405:14 + 405| const d: d as empty, // ERROR: does not match all elements + ^ + +References: + matching.js:396:20 + 396| declare const x: [a: 0, ...]; + ^^^^^^^^^^^ [1] + matching.js:405:19 + 405| const d: d as empty, // ERROR: does not match all elements + ^^^^^ [2] + + Error -------------------------------------------------------------------------------------------------- patterns.js:9:3 Cannot cast `out` to empty because number [1] is incompatible with empty [2]. [incompatible-cast] @@ -454,4 +573,4 @@ References: -Found 28 errors +Found 35 errors diff --git a/tests/match/matching.js b/tests/match/matching.js index 720d287e642..ddd90a3cdff 100644 --- a/tests/match/matching.js +++ b/tests/match/matching.js @@ -282,3 +282,126 @@ const d: d as empty, // ERROR: `type: 'bar'` not checked }; } + +// Disjoint tuple union +{ + declare const x: ['foo', number] + | ['bar', string] + | ['baz', boolean]; + + const e1 = match (x) { + ['foo', const a]: a as number, // OK + ['bar', const a]: a as string, // OK + ['baz', const a]: a as boolean, // OK + const d: d as empty, // OK: all members checked + }; + + const e2 = match (x) { + ['foo', const a]: a as number, // OK + ['bar', const a]: a as string, // OK + const d: d as empty, // ERROR: `'baz'` element not checked + }; + + // Using idents as pattern + declare const foo: 'foo'; + declare const bar: 'bar'; + declare const baz: 'baz'; + const e3 = match (x) { + [foo, const a]: a as number, // OK + [bar, const a]: a as string, // OK + [baz, const a]: a as boolean, // OK + const d: d as empty, // OK: all members checked + }; +} + +// Combo union of tuples with sentinel property and primitive value +{ + declare const x: null | ['bar', number] | ['foo', string]; + + const e1 = match (x) { + ['bar', const a]: a as number, // OK + ['foo', const a]: a as string, // OK + null: 0, + const d: d as empty, // OK: all members checked + }; +} + +// Tuple length refinements +{ + declare const x: [number] + | [string, string] + | [boolean, boolean, boolean]; + + const e1 = match (x) { + [const a]: a as number, // OK + [const a, _]: a as string, // OK + [const a, _, _]: a as empty, // ERROR: `boolean` is not `empty` + const d: d as empty, // OK: all members checked + }; + + const e2 = match (x) { + [...]: 0, // OK: matches all + const d: d as empty, // OK: all members checked + } + + const e3 = match (x) { + [const a, _, ...]: a as string | boolean, // OK + [const a, ...]: a as number, // OK + const d: d as empty, // OK: all members checked + } +} +{ + declare const x: [number] | Array; + + const e1 = match (x) { + []: 0, // OK + [const a]: a as string, // ERROR: `number` is not `string` + [const a, _]: a as string, // OK + const d: d as Array, // OK: tuple checked, but array could have other lengths + }; + + const e2 = match (x) { + [...]: 0, // OK: matches all + const d: d as empty, // OK: all members checked + } +} + +// Optional tuple elements +{ + declare const x: [a: 0, b?: 1, c?: 2]; + + const e1 = match (x) { + [_, ...]: 0, + const d: d as empty, // OK: all elements matched + }; + + const e2 = match (x) { + [_, _, ...]: 0, + const d: d as empty, // ERROR: does not match all possibilities + }; + + const e3 = match (x) { + [_]: 0, + const d: d as empty, // ERROR: does not match all possibilities + }; + + const e4 = match (x) { + [_, _, _]: 0, + const d: d as empty, // ERROR: does not match all possibilities + }; +} + +// Inexact tuple types +{ + declare const x: [a: 0, ...]; + + const e1 = match (x) { + [_, ...]: 0, + const d: d as empty, // OK: all elements matched + }; + + const e2 = match (x) { + [_]: 0, + const d: d as empty, // ERROR: does not match all elements + }; +}