diff --git a/src/common/reason.ml b/src/common/reason.ml index 24c41b3ac0a..cec624eb8e6 100644 --- a/src/common/reason.ml +++ b/src/common/reason.ml @@ -244,6 +244,7 @@ type 'loc virtual_reason_desc = | RUninitialized | RPossiblyUninitialized | RUnannotatedNext + | RTypeGuard | RTypeGuardParam of string | RComponent of name | RComponentType @@ -279,8 +280,8 @@ let rec map_desc_locs f = function | RTupleOutOfBoundsAccess _ | RFunction _ | RFunctionType | RFunctionBody | RFunctionCallType | RFunctionUnusedArgument | RJSXChild | RJSXFunctionCall _ | RJSXIdentifier _ | RJSXElementProps _ | RJSXElement _ | RJSXText | RFbt | RUninitialized | RPossiblyUninitialized - | RUnannotatedNext | REmptyArrayElement | RMappedType | RTypeGuardParam _ | RComponent _ - | RComponentType | RInferredUnionElemArray _ ) as r -> + | RUnannotatedNext | REmptyArrayElement | RMappedType | RTypeGuard | RTypeGuardParam _ + | RComponent _ | RComponentType | RInferredUnionElemArray _ ) as r -> r | RFunctionCall desc -> RFunctionCall (map_desc_locs f desc) | RUnknownUnspecifiedProperty desc -> RUnknownUnspecifiedProperty (map_desc_locs f desc) @@ -764,6 +765,7 @@ let rec string_of_desc = function | RUninitialized -> "uninitialized variable" | RPossiblyUninitialized -> "possibly uninitialized variable" | RUnannotatedNext -> "undefined (default `next` of unannotated generator function)" + | RTypeGuard -> "type guard" | RTypeGuardParam s -> spf "type guard parameter `%s`" s | RComponent name -> spf "component %s" (display_string_of_name name) | RComponentType -> "component" @@ -1560,6 +1562,7 @@ let classification_of_reason_desc desc = | RUnionBranching _ | REnum _ | RUnannotatedNext + | RTypeGuard | RTypeGuardParam _ -> `Unclassified diff --git a/src/common/reason.mli b/src/common/reason.mli index fc117350208..94f6d4aca23 100644 --- a/src/common/reason.mli +++ b/src/common/reason.mli @@ -202,6 +202,7 @@ type 'loc virtual_reason_desc = | RUninitialized | RPossiblyUninitialized | RUnannotatedNext + | RTypeGuard | RTypeGuardParam of string | RComponent of name | RComponentType diff --git a/src/common/ty/ty.ml b/src/common/ty/ty.ml index ede1cab10d0..ea2fe7acda3 100644 --- a/src/common/ty/ty.ml +++ b/src/common/ty/ty.ml @@ -155,7 +155,7 @@ and fun_t = { and return_t = | ReturnType of t - | TypeGuard of string * t + | TypeGuard of bool (* implies *) * string * t and obj_kind = | ExactObj diff --git a/src/common/ty/ty_debug.ml b/src/common/ty/ty_debug.ml index e9c0e1df669..f6aac49fc4a 100644 --- a/src/common/ty/ty_debug.ml +++ b/src/common/ty/ty_debug.ml @@ -196,7 +196,14 @@ struct and dump_return_t ~depth t = match t with | ReturnType t -> dump_t ~depth t - | TypeGuard (x, t) -> spf "%s is %s" x (dump_t ~depth t) + | TypeGuard (impl, x, t) -> + let impl = + if impl then + "implies " + else + "" + in + spf "%s%s is %s" impl x (dump_t ~depth t) and dump_tuple_element ~depth i name t polarity optional = if Base.Option.is_none name && polarity = Neutral && not optional then @@ -601,11 +608,18 @@ struct ] and json_of_return_t = function | ReturnType t -> Hh_json.(JSON_Object [("type_", json_of_t t)]) - | TypeGuard (x, t) -> + | TypeGuard (impl, x, t) -> Hh_json.( JSON_Object [ - ("type_guard", JSON_Object [("type_parameter", JSON_String x); ("type_", json_of_t t)]); + ( "type_guard", + JSON_Object + [ + ("implies", JSON_Bool impl); + ("type_parameter", JSON_String x); + ("type_", json_of_t t); + ] + ); ] ) and json_of_obj_t o = diff --git a/src/common/ty/ty_printer.ml b/src/common/ty/ty_printer.ml index 5f0fe7e469c..0b0d403a471 100644 --- a/src/common/ty/ty_printer.ml +++ b/src/common/ty/ty_printer.ml @@ -309,7 +309,14 @@ let layout_of_elt ~prefer_single_quotes ?(size = 5000) ?(with_comments = true) ~ and return_t ~depth t = match t with | ReturnType t -> type_ ~depth t - | TypeGuard (x, t) -> fuse_with_space [Atom x; Atom "is"; type_ ~depth t] + | TypeGuard (impl, x, t) -> + let impl = + if impl then + [Atom "implies"] + else + [] + in + fuse_with_space (impl @ [Atom x; Atom "is"; type_ ~depth t]) and type_object_property = let to_key x = if property_key_quotes_needed x then diff --git a/src/common/ty/ty_serializer.ml b/src/common/ty/ty_serializer.ml index f648c396585..39647e2a35b 100644 --- a/src/common/ty/ty_serializer.ml +++ b/src/common/ty/ty_serializer.ml @@ -266,16 +266,16 @@ let type_ options = | ReturnType t -> let%map t = type_ t in Ast.Type.Function.TypeAnnotation t - | TypeGuard (x, t) -> + | TypeGuard (impl, x, t) -> + let kind = + if impl then + T.TypeGuard.Implies + else + T.TypeGuard.Default + in let%map t = type_ t in T.Function.TypeGuard - ( Loc.none, - { - T.TypeGuard.kind = T.TypeGuard.Default; - guard = (id_from_string x, Some t); - comments = None; - } - ) + (Loc.none, { T.TypeGuard.kind; guard = (id_from_string x, Some t); comments = None }) and obj_ o = let%bind properties = mapM obj_prop o.obj_props in let%map (exact, inexact, properties) = diff --git a/src/parser_utils/type_sig/type_sig.ml b/src/parser_utils/type_sig/type_sig.ml index 207a4b6fcd0..3d1f3d9bcfa 100644 --- a/src/parser_utils/type_sig/type_sig.ml +++ b/src/parser_utils/type_sig/type_sig.ml @@ -76,7 +76,12 @@ type ('key, 'loc, 'a) predicate = type ('loc, 'a) predicate_or_type_guard = | Predicate of 'loc * (string, 'loc, 'a) predicate option - | TypeGuard of (('loc * string) * 'a) + | TypeGuard of { + loc: 'loc; + param_name: 'loc * string; + type_guard: 'a; + one_sided: bool; + } [@@deriving iter, map, show { with_path = false }] type ('loc, 'a) tparam = diff --git a/src/parser_utils/type_sig/type_sig_parse.ml b/src/parser_utils/type_sig/type_sig_parse.ml index 5144db2dcda..330fbf0a2f1 100644 --- a/src/parser_utils/type_sig/type_sig_parse.ml +++ b/src/parser_utils/type_sig/type_sig_parse.ml @@ -1577,19 +1577,21 @@ and tuple_element opts scope tbls xs (loc, el) = TupleSpread { loc; name; t } and type_guard_opt opts scope tbls xs guard = - let (_, { T.TypeGuard.kind = _; guard = (x, t_opt); _ }) = guard in + let (gloc, { T.TypeGuard.kind; guard = (x, t_opt); _ }) = guard in match t_opt with | Some t -> + let gloc = push_loc tbls gloc in let (loc, { Ast.Identifier.name; _ }) = x in let loc = push_loc tbls loc in - Some ((loc, name), annot opts scope tbls xs t) + Some (gloc, (loc, name), annot opts scope tbls xs t, kind = T.TypeGuard.Implies) | None -> (* TODO(pvekris) support assert type guards in type_sig_parse *) None and type_guard_or_predicate_of_type_guard opts scope tbls xs guard = match type_guard_opt opts scope tbls xs guard with - | Some p -> Some (TypeGuard p) + | Some (loc, param_name, type_guard, one_sided) -> + Some (TypeGuard { loc; param_name; type_guard; one_sided }) | None -> None and return_annot opts scope tbls xs = function @@ -3555,9 +3557,9 @@ and function_def_helper = let predicate = let open Option.Let_syntax in match type_guard_opt with - | Some (loc, p) -> + | Some (loc, param_name, type_guard, one_sided) -> (* Type-guard and %checks cannot coexist (parse error) *) - Some (TypeGuard (loc, p)) + Some (TypeGuard { loc; param_name; type_guard; one_sided }) | None -> let%map (loc, p) = predicate opts scope tbls ps body p in Predicate (loc, p) diff --git a/src/services/type_info/signature_help.ml b/src/services/type_info/signature_help.ml index 4d4f29b0e29..5544d7d6e0a 100644 --- a/src/services/type_info/signature_help.ml +++ b/src/services/type_info/signature_help.ml @@ -20,8 +20,15 @@ let string_of_ty = Ty_printer.string_of_t_single_line ~with_comments:false let string_of_return_t ~exact_by_default = function | Ty.ReturnType t -> Ty_printer.string_of_t_single_line ~exact_by_default ~with_comments:false t - | Ty.TypeGuard (x, t) -> - x ^ " is " ^ Ty_printer.string_of_t_single_line ~exact_by_default ~with_comments:false t + | Ty.TypeGuard (impl, x, t) -> + let impl = + if impl then + "implies " + else + "" + in + let t = Ty_printer.string_of_t_single_line ~exact_by_default ~with_comments:false t in + Utils_js.spf "%s%s is %s" impl x t let documentation_of_param_infos name : Jsdoc.Param.t -> string = let open Jsdoc.Param in diff --git a/src/typing/debug_js.ml b/src/typing/debug_js.ml index 6026911b96b..541165b4ec9 100644 --- a/src/typing/debug_js.ml +++ b/src/typing/debug_js.ml @@ -192,8 +192,15 @@ let rec dump_t_ (depth, tvars) cx t = (kid return_t) (match predicate with | Some (PredBased _) -> " %checks" - | Some (TypeGuardBased { param_name = (_, name); type_guard }) -> - spf " %s is %s" name (kid type_guard) + | Some (TypeGuardBased { reason = _; one_sided; param_name = (_, name); type_guard }) + -> + let implies = + if one_sided then + "implies " + else + "" + in + spf " %s%s is %s" implies name (kid type_guard) | None -> "") ) t @@ -1546,6 +1553,8 @@ let dump_error_message = spf "EPredicateInvalidParameter (%s)" (dump_reason cx r) | ETypeGuardIndexMismatch { use_op; _ } -> spf "ETypeGuardIndexMismatch (%s)" (string_of_use_op use_op) + | ETypeGuardImpliesMismatch { use_op; _ } -> + spf "ETypeGuardImpliesMismatch (%s)" (string_of_use_op use_op) | ETypeGuardParamUnbound _ -> "ETypeGuardParamUnbound" | ETypeGuardFunctionInvalidWrites _ -> "ETypeGuardFunctionInvalidWrites" | ETypeGuardFunctionParamHavoced _ -> "ETypeGuardFunctionParamHavoced" diff --git a/src/typing/errors/error_message.ml b/src/typing/errors/error_message.ml index 9d1e76e17ee..90d9f3c11d8 100644 --- a/src/typing/errors/error_message.ml +++ b/src/typing/errors/error_message.ml @@ -233,6 +233,10 @@ and 'loc t' = use_op: 'loc virtual_use_op; reasons: 'loc virtual_reason * 'loc virtual_reason; } + | ETypeGuardImpliesMismatch of { + use_op: 'loc virtual_use_op; + reasons: 'loc virtual_reason * 'loc virtual_reason; + } | ETypeGuardParamUnbound of 'loc virtual_reason | ETypeGuardFunctionInvalidWrites of { reason: 'loc virtual_reason; @@ -965,6 +969,9 @@ let rec map_loc_of_error_message (f : 'a -> 'b) : 'a t' -> 'b t' = { pred_reason = map_reason pred_reason; binding_reason = map_reason binding_reason } | ETypeGuardIndexMismatch { use_op; reasons = (r1, r2) } -> ETypeGuardIndexMismatch { use_op = map_use_op use_op; reasons = (map_reason r1, map_reason r2) } + | ETypeGuardImpliesMismatch { use_op; reasons = (r1, r2) } -> + ETypeGuardImpliesMismatch + { use_op = map_use_op use_op; reasons = (map_reason r1, map_reason r2) } | ETypeGuardParamUnbound reason -> ETypeGuardParamUnbound (map_reason reason) | ETypeGuardFunctionInvalidWrites { reason; type_guard_reason; write_locs } -> ETypeGuardFunctionInvalidWrites @@ -1440,6 +1447,7 @@ let util_use_op_of_msg nope util = function | EPredicateFuncIncompatibility _ | EPredicateInvalidParameter _ | ETypeGuardIndexMismatch _ + | ETypeGuardImpliesMismatch _ | EInternal (_, _) | EUnsupportedSyntax (_, _) | EUseArrayLiteral _ @@ -1803,7 +1811,8 @@ let loc_of_msg : 'loc t' -> 'loc option = function | EPrimitiveAsInterface _ | EPredicateFuncArityMismatch _ | EPredicateFuncIncompatibility _ - | ETypeGuardIndexMismatch _ -> + | ETypeGuardIndexMismatch _ + | ETypeGuardImpliesMismatch _ -> None let kind_of_msg = @@ -2274,6 +2283,14 @@ let friendly_message_of_msg = function use_op; explanation = None; } + | ETypeGuardImpliesMismatch { use_op; reasons = (lower, upper) } -> + UseOp + { + loc = loc_of_reason lower; + message = MessageTypeGuardImpliesMismatch { lower; upper }; + use_op; + explanation = None; + } | ETypeGuardParamUnbound reason -> Normal (MessageInvalidTypeGuardParamUnbound reason) | ETypeGuardFunctionInvalidWrites { reason = _; type_guard_reason; write_locs } -> Normal (MessageInvalidTypeGuardFunctionWritten { type_guard_reason; write_locs }) @@ -2873,6 +2890,7 @@ let error_code_of_message err : error_code option = | EPredicateFuncIncompatibility _ | EPredicateInvalidParameter _ | ETypeGuardIndexMismatch _ + | ETypeGuardImpliesMismatch _ | ETypeGuardParamUnbound _ | ETypeGuardFunctionInvalidWrites _ | ETypeGuardFunctionParamHavoced _ diff --git a/src/typing/errors/flow_intermediate_error.ml b/src/typing/errors/flow_intermediate_error.ml index 1c2514cd3c0..5b4eb273611 100644 --- a/src/typing/errors/flow_intermediate_error.ml +++ b/src/typing/errors/flow_intermediate_error.ml @@ -3653,6 +3653,8 @@ let to_printable_error : ] | MessageTypeGuardIndexMismatch { lower; upper } -> [ref lower; text " does not appear in the same position as "; ref upper] + | MessageTypeGuardImpliesMismatch { lower; upper } -> + [text "one-sided "; ref lower; text " is incompatible with default "; ref upper] | MessageUnexpectedTemporaryBaseType -> [text "The type argument of a temporary base type must be a compatible literal type"] | MessageUnexpectedUseOfThisType -> [text "Unexpected use of "; code "this"; text " type."] diff --git a/src/typing/errors/flow_intermediate_error_types.ml b/src/typing/errors/flow_intermediate_error_types.ml index ba9e3d8f63b..598ee669bc9 100644 --- a/src/typing/errors/flow_intermediate_error_types.ml +++ b/src/typing/errors/flow_intermediate_error_types.ml @@ -873,6 +873,10 @@ type 'loc message = lower: 'loc virtual_reason; upper: 'loc virtual_reason; } + | MessageTypeGuardImpliesMismatch of { + lower: 'loc virtual_reason; + upper: 'loc virtual_reason; + } | MessageUnclearType | MessageUnderconstrainedImplicitInstantiaton of { reason_call: 'loc virtual_reason; diff --git a/src/typing/flow_js.ml b/src/typing/flow_js.ml index 7fd68252f09..8e92843c7f9 100644 --- a/src/typing/flow_js.ml +++ b/src/typing/flow_js.ml @@ -2620,15 +2620,20 @@ struct | Some p -> rec_flow cx trace (tin, PredicateT (p, tout)) | None -> rec_flow_t ~use_op:unknown_use cx trace (tin, OpenT tout) end - | (Some (Some name, _), TypeGuardBased { param_name = (_, param_name); type_guard }) -> + | ( Some (Some name, _), + TypeGuardBased { reason = _; one_sided; param_name = (_, param_name); type_guard } + ) -> let t = if param_name <> name then (* This is not the refined parameter. *) tin else if sense then intersect cx tin type_guard - else + else if not one_sided then type_guard_diff cx tin type_guard + else + (* Do not refine else branch on one-sided type-guard *) + tin in rec_flow_t ~use_op:unknown_use cx trace (t, OpenT tout) end diff --git a/src/typing/statement.ml b/src/typing/statement.ml index 225967d6fe7..bae06cb431c 100644 --- a/src/typing/statement.ml +++ b/src/typing/statement.ml @@ -7531,7 +7531,7 @@ module Make in (* This check to be performed after the function has been checked to ensure all * entries have been prepared for type checking. *) - let check_type_guard_consistency cx param_loc tg_param tg_reason type_guard = + let check_type_guard_consistency cx _one_sided param_loc tg_param tg_reason type_guard = let env = Context.environment cx in let { Loc_env.var_info; _ } = env in let { Env_api.type_guard_consistency_maps; _ } = var_info in @@ -7597,21 +7597,21 @@ module Make | (loc, Rest) -> err_with_desc (RRestParameter (Some name)) expr_reason loc | (loc, Select _) -> err_with_desc (RPatternParameter name) expr_reason loc in - let type_guard_based_checks tg_param type_guard binding_opt = + let type_guard_based_checks one_sided tg_param type_guard binding_opt = let (name_loc, name) = tg_param in let tg_reason = mk_reason (RTypeGuardParam name) name_loc in let open Pattern_helper in match binding_opt with | None -> Flow_js_utils.add_output cx Error_message.(ETypeGuardParamUnbound tg_reason) | Some (param_loc, Root) -> - check_type_guard_consistency cx param_loc tg_param tg_reason type_guard + check_type_guard_consistency cx one_sided param_loc tg_param tg_reason type_guard | Some binding -> error_on_non_root_binding name tg_reason binding in match pred with - | TypeGuardBased { param_name; type_guard } -> + | TypeGuardBased { reason = _; one_sided; param_name; type_guard } -> let bindings = Pattern_helper.bindings_of_params params in let matching_binding = SMap.find_opt (snd param_name) bindings in - type_guard_based_checks param_name type_guard matching_binding + type_guard_based_checks one_sided param_name type_guard matching_binding | PredBased (expr_reason, (lazy (p_map, _))) -> let required_bindings = Base.List.filter_map (Key_map.keys p_map) ~f:(function @@ -7732,17 +7732,11 @@ module Make let (t, ast_annot) = Anno.mk_type_available_annotation cx tparams_map annot in (Annotated t, Ast.Function.ReturnAnnot.Available ast_annot, None) | ( Ast.Function.ReturnAnnot.TypeGuard - ( loc, - ( gloc, - { - Ast.Type.TypeGuard.guard = (id_name, Some t); - kind = Ast.Type.TypeGuard.Default as kind; - comments; - } - ) - ), + (loc, (gloc, { Ast.Type.TypeGuard.guard = (id_name, Some t); kind; comments })), _ - ) -> + ) + when kind = Ast.Type.TypeGuard.Default + || (kind = Ast.Type.TypeGuard.Implies && Context.one_sided_type_guards cx) -> let (bool_t, guard', predicate) = Anno.convert_type_guard cx diff --git a/src/typing/subtyping_kit.ml b/src/typing/subtyping_kit.ml index 4e191b66a00..75ec85259b9 100644 --- a/src/typing/subtyping_kit.ml +++ b/src/typing/subtyping_kit.ml @@ -148,19 +148,24 @@ module Make (Flow : INPUT) : OUTPUT = struct ) let func_type_guard_compat cx trace use_op grd1 grd2 = - let (params1, (loc1, x1), t1) = grd1 in - let (params2, (loc2, x2), t2) = grd2 in + let (reason1, params1, impl1, (loc1, x1), t1) = grd1 in + let (reason2, params2, impl2, (loc2, x2), t2) = grd2 in + if impl1 && not impl2 then + add_output + cx + ~trace + (Error_message.ETypeGuardImpliesMismatch { use_op; reasons = (reason1, reason2) }); let idx1 = index_of_param params1 x1 in let idx2 = index_of_param params2 x2 in let use_op = Frame (TypePredicateCompatibility, use_op) in - let lower = Reason.mk_reason (RTypeGuardParam x1) loc1 in - let upper = Reason.mk_reason (RTypeGuardParam x2) loc2 in - if idx1 <> idx2 then + ( if idx1 <> idx2 then + let lower = Reason.mk_reason (RTypeGuardParam x1) loc1 in + let upper = Reason.mk_reason (RTypeGuardParam x2) loc2 in add_output cx ~trace - (Error_message.ETypeGuardIndexMismatch { use_op; reasons = (lower, upper) }); - + (Error_message.ETypeGuardIndexMismatch { use_op; reasons = (lower, upper) }) + ); rec_flow_t cx trace ~use_op (t1, t2) let flow_obj_to_obj cx trace ~use_op (lreason, l_obj) (ureason, u_obj) = @@ -1632,10 +1637,17 @@ module Make (Flow : INPUT) : OUTPUT = struct (Error_message.EPredicateFuncIncompatibility { use_op; reasons = (lreason, ureason) }) | (Some (PredBased p1), Some (PredBased p2)) -> func_predicate_compat cx trace use_op (lreason, ft1.params, p1) (ureason, ft2.params, p2) - | ( Some (TypeGuardBased { param_name = x1; type_guard = t1 }), - Some (TypeGuardBased { param_name = x2; type_guard = t2 }) + | ( Some + (TypeGuardBased { reason = r1; one_sided = impl1; param_name = x1; type_guard = t1 }), + Some + (TypeGuardBased { reason = r2; one_sided = impl2; param_name = x2; type_guard = t2 }) ) -> - func_type_guard_compat cx trace use_op (ft1.params, x1, t1) (ft2.params, x2, t2) + func_type_guard_compat + cx + trace + use_op + (r1, ft1.params, impl1, x1, t1) + (r2, ft2.params, impl2, x2, t2) | (Some _, None) | (None, None) -> () diff --git a/src/typing/ty_normalizer.ml b/src/typing/ty_normalizer.ml index fd7cd9759a3..414f0353207 100644 --- a/src/typing/ty_normalizer.ml +++ b/src/typing/ty_normalizer.ml @@ -917,9 +917,9 @@ module Make (I : INPUT) : S = struct let%bind fun_rest_param = fun_rest_param_t ~env rest_param in let%bind fun_return = match predicate with - | Some (T.TypeGuardBased { param_name = (_, x); type_guard = t }) -> + | Some (T.TypeGuardBased { reason = _; one_sided; param_name = (_, x); type_guard = t }) -> let%map t = type__ ~env t in - Ty.TypeGuard (x, t) + Ty.TypeGuard (one_sided, x, t) | Some (T.PredBased _) | None -> let%map t = type__ ~env return_t in diff --git a/src/typing/type.ml b/src/typing/type.ml index acc7678307d..609a0e91e9c 100644 --- a/src/typing/type.ml +++ b/src/typing/type.ml @@ -1208,8 +1208,10 @@ module rec TypeTerm : sig and fun_predicate = | PredBased of (reason * (predicate Key_map.t * predicate Key_map.t) Lazy.t) | TypeGuardBased of { + reason: reason; param_name: ALoc.t * string; type_guard: t; + one_sided: bool; } (* FunTs carry around two `this` types, one to be used during subtyping and diff --git a/src/typing/type_annotation.ml b/src/typing/type_annotation.ml index 019f3864a0a..3ab48ba9917 100644 --- a/src/typing/type_annotation.ml +++ b/src/typing/type_annotation.ml @@ -2439,7 +2439,11 @@ module Make (ConsGen : Type_annotation_sig.ConsGen) (Statement : Statement_sig.S let (((_, type_guard), _) as t') = convert env t in let bool_t = BoolT.at gloc in let guard' = (gloc, { T.TypeGuard.guard = (id_name, Some t'); kind; comments }) in - let predicate = Some (TypeGuardBased { param_name = (name_loc, name); type_guard }) in + let one_sided = kind = Ast.Type.TypeGuard.Implies in + let reason = Reason.mk_reason RTypeGuard gloc in + let predicate = + Some (TypeGuardBased { reason; one_sided; param_name = (name_loc, name); type_guard }) + in check_guard_type env.cx fparams (name, type_guard); (bool_t, guard', predicate) @@ -2453,7 +2457,7 @@ module Make (ConsGen : Type_annotation_sig.ConsGen) (Statement : Statement_sig.S ( gloc, { T.TypeGuard.guard = (((name_loc, { Ast.Identifier.name; _ }) as x), Some t); - kind = T.TypeGuard.Default as kind; + kind; comments; } ) -> diff --git a/src/typing/type_hint.ml b/src/typing/type_hint.ml index 6a7b1bbc13f..23014132f8a 100644 --- a/src/typing/type_hint.ml +++ b/src/typing/type_hint.ml @@ -288,6 +288,8 @@ and type_of_hint_decomposition cx op reason t = | TypeGuardKind (param_loc, param_name) -> TypeGuardBased { + reason; + one_sided = false; param_name = (param_loc, param_name); type_guard = Unsoundness.unresolved_any reason; } diff --git a/src/typing/type_mapper.ml b/src/typing/type_mapper.ml index 3fd8e64b16b..89c0189b96b 100644 --- a/src/typing/type_mapper.ml +++ b/src/typing/type_mapper.ml @@ -765,12 +765,12 @@ class virtual ['a] t = predicate else PredBased p' - | TypeGuardBased { param_name; type_guard = t } -> + | TypeGuardBased { reason; one_sided; param_name; type_guard = t } -> let t' = self#type_ cx map_cx t in if t' == t then predicate else - TypeGuardBased { param_name; type_guard = t' } + TypeGuardBased { reason; one_sided; param_name; type_guard = t' } method private predicate_maps cx map_cx predicate = let (reason, (lazy (pmap, nmap))) = predicate in diff --git a/src/typing/type_sig_merge.ml b/src/typing/type_sig_merge.ml index 3e2ed0b89b4..203adf5ecfb 100644 --- a/src/typing/type_sig_merge.ml +++ b/src/typing/type_sig_merge.ml @@ -1780,8 +1780,9 @@ and merge_fun match predicate with | None -> None | Some (Predicate (loc, p)) -> Some (Type.PredBased (merge_predicate env file (loc, p))) - | Some (TypeGuard (x, t)) -> - Some (Type.TypeGuardBased { param_name = x; type_guard = merge env file t }) + | Some (TypeGuard { loc; param_name; type_guard = t; one_sided }) -> + let reason = Reason.mk_reason Reason.RTypeGuard loc in + Some (Type.TypeGuardBased { reason; one_sided; param_name; type_guard = merge env file t }) in let this_status = if is_method then diff --git a/src/typing/type_visitor.ml b/src/typing/type_visitor.ml index 2bef19d9aaf..42a90f5387b 100644 --- a/src/typing/type_visitor.ml +++ b/src/typing/type_visitor.ml @@ -353,7 +353,8 @@ class ['a] t = method private fun_predicate cx pole acc predicate = match predicate with | PredBased p -> self#predicate_maps cx acc p - | TypeGuardBased { param_name = _; type_guard = t } -> self#type_ cx pole acc t + | TypeGuardBased { reason = _; one_sided = _; param_name = _; type_guard = t } -> + self#type_ cx pole acc t method private predicate_maps cx acc (_, (lazy (pmap, nmap))) = let acc = Key_map.fold (fun _ p acc -> self#predicate cx acc p) pmap acc in diff --git a/tests/type_at_pos_type_guards/decls.js b/tests/type_at_pos_type_guards/decls.js index 4482b998c95..d7c6a9035f6 100644 --- a/tests/type_at_pos_type_guards/decls.js +++ b/tests/type_at_pos_type_guards/decls.js @@ -9,3 +9,6 @@ function zeros(vals: $ReadOnlyArray<0 | 1>): $ReadOnlyArray<0> { return vals.filter((x): x is 0 => x === 0); // ^ } + +declare function implies(x: mixed): implies x is number; +// ^ diff --git a/tests/type_at_pos_type_guards/type_at_pos_type_guards.exp b/tests/type_at_pos_type_guards/type_at_pos_type_guards.exp index fdcb95fd606..2111f0145b3 100644 --- a/tests/type_at_pos_type_guards/type_at_pos_type_guards.exp +++ b/tests/type_at_pos_type_guards/type_at_pos_type_guards.exp @@ -17,6 +17,11 @@ Returns the elements of an array that meet the condition specified in a callback ) => Array<0> decls.js:9:15,9:20 +decls.js:13:18 +Flags: +(x: mixed) => implies x is number +decls.js:13:18,13:24 + test.js:27:11 Flags: { diff --git a/tests/type_guards/class_exports.js b/tests/type_guards/class_exports.js index e62a29807a2..616fb74135a 100644 --- a/tests/type_guards/class_exports.js +++ b/tests/type_guards/class_exports.js @@ -2,8 +2,12 @@ export class C { m(x: mixed): x is number { return typeof x === 'number'; } + os(x: mixed): implies x is number { + return typeof x === 'number'; + } } declare export class D { m(x: mixed): x is number; + os(x: mixed): implies x is number; } diff --git a/tests/type_guards/class_imports.js b/tests/type_guards/class_imports.js index 6c2494e7f1c..bcf945bf271 100644 --- a/tests/type_guards/class_imports.js +++ b/tests/type_guards/class_imports.js @@ -8,8 +8,18 @@ if (c.m(x)) { (x: string); // error number ~> string } +if (c.os(x)) { + (x: number); + (x: string); // error number ~> string +} + const d = new D(); if (d.m(x)) { (x: number); (x: string); // error number ~> string } + +if (d.os(x)) { + (x: number); + (x: string); // error number ~> string +} diff --git a/tests/type_guards/refinement.js b/tests/type_guards/refinement.js index c087de7d173..094e00183ef 100644 --- a/tests/type_guards/refinement.js +++ b/tests/type_guards/refinement.js @@ -225,3 +225,27 @@ function union_with_any() { (x: empty); // error string ~> empty } } + +function one_sided( + value: number | string, + fn: (x: mixed) => implies x is number, +) { + if (fn(value)) { + (value: number); // okay + (value: string); // error number ~> string + } else { + (value: string); // error number ~> string + (value: number); // error string ~> number + } + + declare class ReadOnlyArray_<+T> { + filter(callbackfn: typeof Boolean): Array<$NonMaybeType>; + filter(predicate: (this: This, value: T, index: number, array: $ReadOnlyArray) => implies value is S, thisArg?: This): Array; + filter(callbackfn: (this : This, value: T, index: number, array: $ReadOnlyArray) => mixed, thisArg : This): Array; + } + + declare var arr: ReadOnlyArray_; + arr.filter(fn) as Array; // okay + arr.filter(fn) as Array; // error number ~> string + +} diff --git a/tests/type_guards/subtyping.js b/tests/type_guards/subtyping.js index b99c0589b90..7878c13b625 100644 --- a/tests/type_guards/subtyping.js +++ b/tests/type_guards/subtyping.js @@ -46,6 +46,18 @@ function type_guard_subtyping_error_3(f: (x: mixed) => x is $ReadOnlyArray return f; // error } +function type_guard_subtyping_one_sided_ok_1(f: (x: mixed) => implies x is A): (x: mixed) => implies x is A { + return f; // okay +} + +function type_guard_subtyping_one_sided_ok_2(f: (x: mixed) => x is A): (x: mixed) => implies x is A { + return f; // okay +} + +function type_guard_subtyping_one_sided_error(f: (x: mixed) => implies x is A): (x: mixed) => x is A { + return f; // error +} + // Unification function type_guard_unif_ok_1(f: Array<(x: mixed) => x is A>): Array<(x: mixed) => x is A> { @@ -63,3 +75,11 @@ function type_guard_unif_error_2(f: Array<(x: mixed) => x is A>): Array<(x: mixe function type_guard_unif_error_3(f: Array<(x: mixed) => x is A>): Array<(x: mixed) => x is C> { return f; // error A ~> C } + +function type_guard_unif_one_sided_ok(f: Array<(x: mixed) => implies x is A>): Array<(x: mixed) => implies x is A> { + return f; // okay +} + +function type_guard_unif_one_sided_error(f: Array<(x: mixed) => x is A>): Array<(x: mixed) => implies x is A> { + return f; // error +} diff --git a/tests/type_guards/type_guards.exp b/tests/type_guards/type_guards.exp index 73f94b1b07c..cda9ece4adb 100644 --- a/tests/type_guards/type_guards.exp +++ b/tests/type_guards/type_guards.exp @@ -15,20 +15,54 @@ References: ^^^^^^ [2] -Error -------------------------------------------------------------------------------------------- class_imports.js:14:4 +Error -------------------------------------------------------------------------------------------- class_imports.js:13:4 Cannot cast `x` to string because number [1] is incompatible with string [2]. [incompatible-cast] - class_imports.js:14:4 - 14| (x: string); // error number ~> string + class_imports.js:13:4 + 13| (x: string); // error number ~> string ^ References: - class_exports.js:8:21 - 8| m(x: mixed): x is number; + class_exports.js:5:30 + 5| os(x: mixed): implies x is number { + ^^^^^^ [1] + class_imports.js:13:7 + 13| (x: string); // error number ~> string + ^^^^^^ [2] + + +Error -------------------------------------------------------------------------------------------- class_imports.js:19:4 + +Cannot cast `x` to string because number [1] is incompatible with string [2]. [incompatible-cast] + + class_imports.js:19:4 + 19| (x: string); // error number ~> string + ^ + +References: + class_exports.js:11:21 + 11| m(x: mixed): x is number; ^^^^^^ [1] - class_imports.js:14:7 - 14| (x: string); // error number ~> string + class_imports.js:19:7 + 19| (x: string); // error number ~> string + ^^^^^^ [2] + + +Error -------------------------------------------------------------------------------------------- class_imports.js:24:4 + +Cannot cast `x` to string because number [1] is incompatible with string [2]. [incompatible-cast] + + class_imports.js:24:4 + 24| (x: string); // error number ~> string + ^ + +References: + class_exports.js:12:30 + 12| os(x: mixed): implies x is number; + ^^^^^^ [1] + class_imports.js:24:7 + 24| (x: string); // error number ~> string ^^^^^^ [2] @@ -1760,6 +1794,82 @@ References: ^^^^^ [2] +Error ---------------------------------------------------------------------------------------------- refinement.js:235:6 + +Cannot cast `value` to string because number [1] is incompatible with string [2]. [incompatible-cast] + + refinement.js:235:6 + 235| (value: string); // error number ~> string + ^^^^^ + +References: + refinement.js:230:10 + 230| value: number | string, + ^^^^^^ [1] + refinement.js:235:13 + 235| (value: string); // error number ~> string + ^^^^^^ [2] + + +Error ---------------------------------------------------------------------------------------------- refinement.js:237:6 + +Cannot cast `value` to string because number [1] is incompatible with string [2]. [incompatible-cast] + + refinement.js:237:6 + 237| (value: string); // error number ~> string + ^^^^^ + +References: + refinement.js:230:10 + 230| value: number | string, + ^^^^^^ [1] + refinement.js:237:13 + 237| (value: string); // error number ~> string + ^^^^^^ [2] + + +Error ---------------------------------------------------------------------------------------------- refinement.js:238:6 + +Cannot cast `value` to number because string [1] is incompatible with number [2]. [incompatible-cast] + + refinement.js:238:6 + 238| (value: number); // error string ~> number + ^^^^^ + +References: + refinement.js:230:19 + 230| value: number | string, + ^^^^^^ [1] + refinement.js:238:13 + 238| (value: number); // error string ~> number + ^^^^^^ [2] + + +Error ---------------------------------------------------------------------------------------------- refinement.js:249:7 + +Cannot call `arr.filter` because: [incompatible-call] + - Either number [1] is incompatible with string [2] in the type predicate. + - Or property `name` is missing in statics of function type [3] but exists in statics of `Boolean` [4]. + + refinement.js:249:7 + 249| arr.filter(fn) as Array; // error number ~> string + ^^^^^^ + +References: + refinement.js:231:34 + 231| fn: (x: mixed) => implies x is number, + ^^^^^^ [1] + refinement.js:249:27 + 249| arr.filter(fn) as Array; // error number ~> string + ^^^^^^ [2] + refinement.js:231:7 + 231| fn: (x: mixed) => implies x is number, + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ [3] + /core.js:376:15 + 376| declare class Boolean { + ^^^^^^^ [4] + + Error --------------------------------------------------------------------------------------------------- subst.js:10:15 Cannot assign `funA(...)` to `x2` because `A` [1] is incompatible with `B` [2]. [incompatible-type] @@ -1918,62 +2028,99 @@ References: ^^^^^^^^ [2] -Error ----------------------------------------------------------------------------------------------- subtyping.js:56:10 +Error ----------------------------------------------------------------------------------------------- subtyping.js:58:10 + +Cannot return `f` because one-sided type guard [1] is incompatible with default type guard [2]. [function-predicate] + + subtyping.js:58:10 + 58| return f; // error + ^ + +References: + subtyping.js:57:64 + 57| function type_guard_subtyping_one_sided_error(f: (x: mixed) => implies x is A): (x: mixed) => x is A { + ^^^^^^^^^^^^^^ [1] + subtyping.js:57:95 + 57| function type_guard_subtyping_one_sided_error(f: (x: mixed) => implies x is A): (x: mixed) => x is A { + ^^^^^^ [2] + + +Error ----------------------------------------------------------------------------------------------- subtyping.js:68:10 Cannot return `f` because `A` [1] is incompatible with `C` [2] in the type predicate of array element. Arrays are invariantly typed. See https://flow.org/en/docs/faq/#why-cant-i-pass-an-arraystring-to-a-function-that-takes-an-arraystring-number. [incompatible-return] - subtyping.js:56:10 - 56| return f; // error C ~ A + subtyping.js:68:10 + 68| return f; // error C ~ A ^ References: - subtyping.js:55:92 - 55| function type_guard_unif_error_1(f: Array<(x: mixed) => x is C>): Array<(x: mixed) => x is A> { + subtyping.js:67:92 + 67| function type_guard_unif_error_1(f: Array<(x: mixed) => x is C>): Array<(x: mixed) => x is A> { ^ [1] - subtyping.js:55:62 - 55| function type_guard_unif_error_1(f: Array<(x: mixed) => x is C>): Array<(x: mixed) => x is A> { + subtyping.js:67:62 + 67| function type_guard_unif_error_1(f: Array<(x: mixed) => x is C>): Array<(x: mixed) => x is A> { ^ [2] -Error ----------------------------------------------------------------------------------------------- subtyping.js:60:10 +Error ----------------------------------------------------------------------------------------------- subtyping.js:72:10 Cannot return `f` because `A` [1] is incompatible with `B` [2] in the type predicate of array element. [incompatible-return] - subtyping.js:60:10 - 60| return f; // errors A ~> B, B ~> A + subtyping.js:72:10 + 72| return f; // errors A ~> B, B ~> A ^ References: - subtyping.js:59:62 - 59| function type_guard_unif_error_2(f: Array<(x: mixed) => x is A>): Array<(x: mixed) => x is B> { + subtyping.js:71:62 + 71| function type_guard_unif_error_2(f: Array<(x: mixed) => x is A>): Array<(x: mixed) => x is B> { ^ [1] - subtyping.js:59:92 - 59| function type_guard_unif_error_2(f: Array<(x: mixed) => x is A>): Array<(x: mixed) => x is B> { + subtyping.js:71:92 + 71| function type_guard_unif_error_2(f: Array<(x: mixed) => x is A>): Array<(x: mixed) => x is B> { ^ [2] -Error ----------------------------------------------------------------------------------------------- subtyping.js:64:10 +Error ----------------------------------------------------------------------------------------------- subtyping.js:76:10 Cannot return `f` because `A` [1] is incompatible with `C` [2] in the type predicate of array element. [incompatible-return] - subtyping.js:64:10 - 64| return f; // error A ~> C + subtyping.js:76:10 + 76| return f; // error A ~> C ^ References: - subtyping.js:63:62 - 63| function type_guard_unif_error_3(f: Array<(x: mixed) => x is A>): Array<(x: mixed) => x is C> { + subtyping.js:75:62 + 75| function type_guard_unif_error_3(f: Array<(x: mixed) => x is A>): Array<(x: mixed) => x is C> { ^ [1] - subtyping.js:63:92 - 63| function type_guard_unif_error_3(f: Array<(x: mixed) => x is A>): Array<(x: mixed) => x is C> { + subtyping.js:75:92 + 75| function type_guard_unif_error_3(f: Array<(x: mixed) => x is A>): Array<(x: mixed) => x is C> { ^ [2] +Error ----------------------------------------------------------------------------------------------- subtyping.js:84:10 + +Cannot return `f` because one-sided type guard [1] is incompatible with default type guard [2] in array element. Arrays +are invariantly typed. See +https://flow.org/en/docs/faq/#why-cant-i-pass-an-arraystring-to-a-function-that-takes-an-arraystring-number. +[function-predicate] + + subtyping.js:84:10 + 84| return f; // error + ^ + +References: + subtyping.js:83:95 + 83| function type_guard_unif_one_sided_error(f: Array<(x: mixed) => x is A>): Array<(x: mixed) => implies x is A> { + ^^^^^^^^^^^^^^ [1] + subtyping.js:83:65 + 83| function type_guard_unif_one_sided_error(f: Array<(x: mixed) => x is A>): Array<(x: mixed) => implies x is A> { + ^^^^^^ [2] + + Error -------------------------------------------------------------------------------- type_guard_compatibility.js:15:30 Cannot use `B` [1] as type prediate for parameter `x` because `B` [1] is incompatible with `A` [2]. A user defined type @@ -2099,7 +2246,7 @@ Cannot find type guard parameter `x` [1] in the parameters of this function (typ -Found 124 errors +Found 132 errors Only showing the most relevant union/intersection branches. To see all branches, re-run Flow with --show-all-branches