Skip to content

Commit

Permalink
[flow] in operator now refines for property existence
Browse files Browse the repository at this point in the history
Summary:
Changelog: [feature] `'key' in x` now refines the type of `x` to objects which have the property `key`.

- `in` checks for both own and non-own properties.
- If we find that a key does not exist in an inexact object or instance/interface, then the negation is not refined since the property may or may not exist.
- An optional property is also considered as if it may or may not exist.
- If a proto/super is a union, every member of the union must have it
- If the input to the refinement is an intersection, one member of that intersection must have it
- We don't refine arrays since it's not useful (check for `.length` instead), and also because Flow doesn't handle array holes

Reviewed By: SamChou19815

Differential Revision: D67107747

fbshipit-source-id: 28f5a936c53b9336e7614169290e5bfb67eaafb1
  • Loading branch information
gkz authored and facebook-github-bot committed Dec 13, 2024
1 parent e084dc2 commit 2e05885
Show file tree
Hide file tree
Showing 12 changed files with 683 additions and 7 deletions.
9 changes: 9 additions & 0 deletions src/analysis/env_builder/env_api.ml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ module type S = sig
prop: string;
other_loc: L.t;
}
| PropExistsR of {
propname: string;
loc: L.t;
}
| PropNullishR of {
propname: string;
loc: L.t;
Expand Down Expand Up @@ -443,6 +447,10 @@ module Make
prop: string;
other_loc: L.t;
}
| PropExistsR of {
propname: string;
loc: L.t;
}
| PropNullishR of {
propname: string;
loc: L.t;
Expand Down Expand Up @@ -685,6 +693,7 @@ module Make
else
lit
| SentinelR { prop; _ } -> Printf.sprintf "SentinelR %s" prop
| PropExistsR { propname; loc = _ } -> Printf.sprintf "PropExistsR (%s)" propname
| PropNullishR { propname = prop; _ } -> Printf.sprintf "PropNullishR %s" prop
| PropIsExactlyNullR { propname = prop; _ } -> Printf.sprintf "PropIsExactlyNullR %s" prop
| PropNonVoidR { propname = prop; _ } -> Printf.sprintf "PropNonVoidR %s" prop
Expand Down
1 change: 1 addition & 0 deletions src/analysis/env_builder/name_def_ordering.ml
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ struct
| SingletonStrR _
| SingletonNumR _
| SingletonBigIntR _
| PropExistsR _
| PropNullishR _
| PropIsExactlyNullR _
| PropNonVoidR _
Expand Down
28 changes: 27 additions & 1 deletion src/analysis/env_builder/name_resolver.ml
Original file line number Diff line number Diff line change
Expand Up @@ -5286,6 +5286,32 @@ module Make (Context : C) (FlowAPIUtils : F with type cx = Context.t) :
left
right

method in_test loc prop_expr obj_expr =
ignore @@ this#expression prop_expr;
ignore
@@ (* We already ensure that RHS of `in` must be object,
* so the expression must be at least truthy as well *)
this#optional_chain_must_be_able_to_refine_base_object_to_non_maybe
~can_refine_obj_prop:OptionalChainingRefinement.CanApplyPropTruthyRefi
obj_expr;
match RefinementKey.of_expression obj_expr with
| None -> ()
| Some obj_refinement_key ->
let propname =
match prop_expr with
| (_, Flow_ast.Expression.StringLiteral { Ast.StringLiteral.value; _ }) -> Some value
| (_, Flow_ast.Expression.NumberLiteral { Ast.NumberLiteral.value; _ })
when Js_number.is_float_safe_integer value ->
Some (Dtoa.ecma_string_of_float value)
| _ -> None
in
Base.Option.iter propname ~f:(fun propname ->
this#add_single_refinement
obj_refinement_key
~refining_locs:(L.LSet.singleton loc)
(PropExistsR { propname; loc })
)

method instance_test loc expr instance =
ignore
@@ (* We already ensure that RHS of instanceof must be object,
Expand Down Expand Up @@ -5325,11 +5351,11 @@ module Make (Context : C) (FlowAPIUtils : F with type cx = Context.t) :
| StrictEqual -> eq_test ~strict:true ~sense:true loc left right
| StrictNotEqual -> eq_test ~strict:true ~sense:false loc left right
| Instanceof -> this#instance_test loc left right
| In -> this#in_test loc left right
| LessThan
| LessThanEqual
| GreaterThan
| GreaterThanEqual
| In
| LShift
| RShift
| RShift3
Expand Down
82 changes: 82 additions & 0 deletions src/typing/predicate_kit.ml
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,9 @@ and predicate_no_concretization cx trace result_collector l ~p =
| NotP TruthyP ->
let filtered = Type_filter.not_truthy cx l in
report_filtering_result_to_predicate_result filtered result_collector
| PropExistsP { propname; reason = _ } -> prop_exists_test cx propname true l result_collector
| NotP (PropExistsP { propname; reason = _ }) ->
prop_exists_test cx propname false l result_collector
| PropTruthyP (key, r) -> prop_truthy_test cx trace key r true l result_collector
| NotP (PropTruthyP (key, r)) -> prop_truthy_test cx trace key r false l result_collector
| PropNonMaybeP (key, r) -> prop_non_maybe_test cx trace key r true l result_collector
Expand Down Expand Up @@ -494,6 +497,85 @@ and type_guard_diff cx t1 t2 =
else
Type_filter.TypeFilterResult { type_ = t1; changed = false }

and prop_exists_test cx key sense obj result_collector =
match has_prop cx (OrdinaryName key) obj with
| Some has ->
if has = sense then
report_unchanged_filtering_result_to_predicate_result obj result_collector
else
report_changes_to_input result_collector
| None -> report_unchanged_filtering_result_to_predicate_result obj result_collector

(**
* If an object has an own or non-own prop, representing `'key' in obj`.
* Returns `None` if it is unknown whether the object has the prop (for example
* due to inexact objects).
*)
and has_prop cx key obj =
let all_have_prop xs =
Base.List.fold xs ~init:(Some true) ~f:(fun acc x ->
match (acc, x) with
| (None, _)
| (_, None) ->
None
| (Some a, Some b) -> Some (a && b)
)
in
let some_has_prop xs =
Base.List.fold xs ~init:(Some false) ~f:(fun acc x ->
match (acc, x) with
| (Some true, None)
| (None, Some true) ->
Some true
| (_, None)
| (None, _) ->
None
| (Some a, Some b) -> Some (a || b)
)
in
let find_key ~exact ~super ~props_list key =
let current_has_prop =
Base.List.map props_list ~f:(fun props ->
match Context.get_prop cx props key with
| Some prop ->
(match prop with
| Field { type_; _ } when Slice_utils.is_prop_optional type_ ->
(* If a field is optional, it is unknown whether it exists. *)
None
| _ -> Some true)
| None -> Some false
)
|> some_has_prop
in
match current_has_prop with
| Some true -> Some true
| Some false
| None ->
let super_ts = possible_concrete_types_for_inspection cx (TypeUtil.reason_of_t super) super in
let super_has_prop = Base.List.map super_ts ~f:(has_prop cx key) |> all_have_prop in
(match super_has_prop with
| Some true -> Some true
| Some false when not exact -> None
| Some false -> current_has_prop
| None -> None)
in
match obj with
| DefT (_, ObjT { flags; props_tmap; proto_t; _ }) ->
find_key ~exact:(Obj_type.is_exact flags.obj_kind) ~super:proto_t ~props_list:[props_tmap] key
| DefT (_, InstanceT { super; inst = { own_props; proto_props; _ }; _ }) ->
find_key ~exact:false ~super ~props_list:[own_props; proto_props] key
| NullProtoT _ -> Some false
| ObjProtoT _ -> Some (is_object_prototype_method key)
| FunProtoT _ -> Some (is_function_prototype key)
| IntersectionT (reason, rep) ->
InterRep.members rep
|> Base.List.map ~f:(fun t ->
let ts = possible_concrete_types_for_inspection cx reason t in
Base.List.map ts ~f:(has_prop cx key) |> all_have_prop
)
|> some_has_prop
| _ -> None

and prop_truthy_test cx trace key reason sense obj result_collector =
prop_exists_test_generic
key
Expand Down
8 changes: 4 additions & 4 deletions src/typing/slice_utils.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1804,6 +1804,10 @@ let mk_mapped_prop_type ~use_op ~mapped_type_optionality ~poly_prop key_t prop_o
else
t

let is_prop_optional = function
| OptionalT _ -> true
| _ -> false

let map_object
poly_prop
{ variance; optional = mapped_type_optionality }
Expand All @@ -1818,10 +1822,6 @@ let map_object
| Polarity.Neutral -> prop_polarity
| _ -> variance
in
let is_prop_optional = function
| OptionalT _ -> true
| _ -> false
in
let props =
let keys =
match selected_keys_and_indexers with
Expand Down
6 changes: 6 additions & 0 deletions src/typing/type.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1013,6 +1013,11 @@ module rec TypeTerm : sig
| SymbolP of ALoc.t (* symbol *)
| VoidP (* undefined *)
| ArrP (* Array.isArray *)
(* `if ('b' in a)` yields `flow (a, PredicateT(PropExistsP ("b"), tout))` *)
| PropExistsP of {
propname: string;
reason: reason;
}
(* `if (a.b)` yields `flow (a, PredicateT(PropTruthyP ("b"), tout))` *)
| PropTruthyP of string * reason
(* `if (a?.b === null)` yields `flow (a, PredicateT(PropIsExactlyNullP ("b"), tout))` *)
Expand Down Expand Up @@ -4283,6 +4288,7 @@ let rec string_of_predicate = function
| SymbolP _ -> "symbol"
(* Array.isArray *)
| ArrP -> "array"
| PropExistsP { propname; _ } -> spf "prop `%s` exists" propname
| PropTruthyP (key, _) -> spf "prop `%s` is truthy" key
| PropIsExactlyNullP (key, _) -> spf "prop `%s` is exactly null" key
| PropNonVoidP (key, _) -> spf "prop `%s` is not undefined" key
Expand Down
1 change: 1 addition & 0 deletions src/typing/typeUtil.ml
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,7 @@ let rec eq_predicate (p1, p2) =
| (SingletonStrP (_, s1, v1), SingletonStrP (_, s2, v2)) -> s1 = s2 && v1 = v2
| (SingletonNumP (_, s1, v1), SingletonNumP (_, s2, v2)) -> s1 = s2 && v1 = v2
| (LatentP (c1, i1), LatentP (c2, i2)) -> c1 == c2 && i1 = i2
| (PropExistsP { propname = s1; _ }, PropExistsP { propname = s2; _ }) -> s1 = s2
| (PropTruthyP (s1, _), PropTruthyP (s2, _)) -> s1 = s2
| (PropNonMaybeP (s1, _), PropNonMaybeP (s2, _)) -> s1 = s2
(* Complex *)
Expand Down
3 changes: 3 additions & 0 deletions src/typing/type_env.ml
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,9 @@ let predicate_of_refinement cx =
Some (PropNonVoidP (propname, mk_reason (RProperty (Some (OrdinaryName propname))) loc))
| PropTruthyR { propname; loc } ->
Some (PropTruthyP (propname, mk_reason (RProperty (Some (OrdinaryName propname))) loc))
| PropExistsR { propname; loc } ->
let reason = mk_reason (RProperty (Some (OrdinaryName propname))) loc in
Some (PropExistsP { propname; reason })
in
pred

Expand Down
3 changes: 2 additions & 1 deletion src/typing/type_mapper.ml
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,8 @@ class virtual ['a] t =
| PropNonMaybeP _
| PropNonVoidP _
| PropIsExactlyNullP _
| PropTruthyP _ ->
| PropTruthyP _
| PropExistsP _ ->
p
| LatentP ((lazy (use_op, loc, t, targs, argts)), i) ->
let t' = self#type_ cx map_cx t in
Expand Down
1 change: 1 addition & 0 deletions src/typing/type_visitor.ml
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ class ['a] t =
| VoidP -> acc
| ArrP -> acc
| PropTruthyP _ -> acc
| PropExistsP _ -> acc
| PropNonVoidP _ -> acc
| PropIsExactlyNullP _ -> acc
| PropNonMaybeP _ -> acc
Expand Down
Loading

0 comments on commit 2e05885

Please sign in to comment.