diff --git a/hphp/hack/src/decl/decl_hint.ml b/hphp/hack/src/decl/decl_hint.ml index b48acb3bbf1d4..5c18c316337b1 100644 --- a/hphp/hack/src/decl/decl_hint.ml +++ b/hphp/hack/src/decl/decl_hint.ml @@ -21,9 +21,8 @@ let rec hint env (p, h) = let h = hint_ p env h in Typing_reason.Rhint p, h -(* TODO(tingley): Record the optional status and use this to reconcile types. *) -and shape_field_info_to_shape_field_type env { sfi_optional=_; sfi_hint } = - hint env sfi_hint +and shape_field_info_to_shape_field_type env { sfi_optional; sfi_hint } = + { sft_optional = sfi_optional; sft_ty = hint env sfi_hint } and hint_ p env = function | Hany -> Tany diff --git a/hphp/hack/src/decl/decl_instantiate.ml b/hphp/hack/src/decl/decl_instantiate.ml index f410580978103..18ab32ffb1724 100644 --- a/hphp/hack/src/decl/decl_instantiate.ml +++ b/hphp/hack/src/decl/decl_instantiate.ml @@ -100,7 +100,7 @@ and instantiate_ subst x = let tyl = List.map tyl (instantiate subst) in Tapply (x, tyl) | Tshape (fields_known, fdm) -> - let fdm = Nast.ShapeMap.map (instantiate subst) fdm in + let fdm = ShapeFieldMap.map (instantiate subst) fdm in Tshape (fields_known, fdm) let instantiate_ce subst ({ ce_type = x; _ } as ce) = diff --git a/hphp/hack/src/decl/decl_pos_utils.ml b/hphp/hack/src/decl/decl_pos_utils.ml index 83e72d94e4401..aa7692b486d1b 100644 --- a/hphp/hack/src/decl/decl_pos_utils.ml +++ b/hphp/hack/src/decl/decl_pos_utils.ml @@ -1,12 +1,12 @@ (** - * Copyright (c) 2015, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the "hack" directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - *) +* Copyright (c) 2015, Facebook, Inc. +* All rights reserved. +* +* This source code is licensed under the BSD-style license found in the +* LICENSE file in the "hack" directory of this source tree. An additional grant +* of patent rights can be found in the PATENTS file in the same directory. +* +*) open Core open Decl_defs @@ -16,74 +16,74 @@ module ShapeMap = Nast.ShapeMap (*****************************************************************************) (* Functor traversing a type, but applies a user defined function for - * positions. - *) +* positions. +*) (*****************************************************************************) module TraversePos(ImplementPos: sig val pos: Pos.t -> Pos.t end) = struct - open Typing_reason - - let pos = ImplementPos.pos - - let rec reason = function - | Rnone -> Rnone - | Rwitness p -> Rwitness (pos p) - | Ridx (p, r) -> Ridx (pos p, reason r) - | Ridx_vector p -> Ridx_vector (pos p) - | Rappend p -> Rappend (pos p) - | Rfield p -> Rfield (pos p) - | Rforeach p -> Rforeach (pos p) - | Rasyncforeach p -> Rasyncforeach (pos p) - | Raccess p -> Raccess (pos p) - | Rarith p -> Rarith (pos p) - | Rarith_ret p -> Rarith_ret (pos p) - | Rarray_plus_ret p -> Rarray_plus_ret (pos p) - | Rstring2 p -> Rstring2 (pos p) - | Rcomp p -> Rcomp (pos p) - | Rconcat p -> Rconcat (pos p) - | Rconcat_ret p -> Rconcat_ret (pos p) - | Rlogic p -> Rlogic (pos p) - | Rlogic_ret p -> Rlogic_ret (pos p) - | Rbitwise p -> Rbitwise (pos p) - | Rbitwise_ret p -> Rbitwise_ret (pos p) - | Rstmt p -> Rstmt (pos p) - | Rno_return p -> Rno_return (pos p) - | Rno_return_async p -> Rno_return_async (pos p) - | Rret_fun_kind (p, k) -> Rret_fun_kind (pos p, k) - | Rhint p -> Rhint (pos p) - | Rnull_check p -> Rnull_check (pos p) - | Rnot_in_cstr p -> Rnot_in_cstr (pos p) - | Rthrow p -> Rthrow (pos p) - | Rplaceholder p -> Rplaceholder (pos p) - | Rattr p -> Rattr (pos p) - | Rxhp p -> Rxhp (pos p) - | Rret_div p -> Rret_div (pos p) - | Ryield_gen p -> Ryield_gen (pos p) - | Ryield_asyncgen p -> Ryield_asyncgen (pos p) - | Ryield_asyncnull p -> Ryield_asyncnull (pos p) - | Ryield_send p -> Ryield_send (pos p) - | Rlost_info (s, r1, p2) -> Rlost_info (s, reason r1, pos p2) - | Rcoerced (p1, p2, x) -> Rcoerced (pos p1, pos p2, x) - | Rformat (p1, s, r) -> Rformat (pos p1, s, reason r) - | Rclass_class (p, s) -> Rclass_class (pos p, s) - | Runknown_class p -> Runknown_class (pos p) - | Rdynamic_yield (p1, p2, s1, s2) -> Rdynamic_yield(pos p1, pos p2, s1, s2) - | Rmap_append p -> Rmap_append (pos p) - | Rvar_param p -> Rvar_param (pos p) - | Runpack_param p -> Runpack_param (pos p) - | Rinstantiate (r1,x,r2) -> Rinstantiate (reason r1, x, reason r2) - | Rarray_filter (p, r) -> Rarray_filter (pos p, reason r) - | Rtype_access (r1, x, r2) -> Rtype_access (reason r1, x, reason r2) - | Rexpr_dep_type (r, p, n) -> Rexpr_dep_type (reason r, pos p, n) - | Rnullsafe_op p -> Rnullsafe_op (pos p) - | Rtconst_no_cstr (p, s) -> Rtconst_no_cstr (pos p, s) - | Rused_as_map p -> Rused_as_map (pos p) - | Rused_as_shape p -> Rused_as_shape (pos p) - | Rpredicated (p, f) -> Rpredicated (pos p, f) - | Rinstanceof (p, f) -> Rinstanceof (pos p, f) - let string_id (p, x) = pos p, x - - let rec ty (p, x) = - reason p, ty_ x +open Typing_reason + +let pos = ImplementPos.pos + +let rec reason = function + | Rnone -> Rnone + | Rwitness p -> Rwitness (pos p) + | Ridx (p, r) -> Ridx (pos p, reason r) + | Ridx_vector p -> Ridx_vector (pos p) + | Rappend p -> Rappend (pos p) + | Rfield p -> Rfield (pos p) + | Rforeach p -> Rforeach (pos p) + | Rasyncforeach p -> Rasyncforeach (pos p) + | Raccess p -> Raccess (pos p) + | Rarith p -> Rarith (pos p) + | Rarith_ret p -> Rarith_ret (pos p) + | Rarray_plus_ret p -> Rarray_plus_ret (pos p) + | Rstring2 p -> Rstring2 (pos p) + | Rcomp p -> Rcomp (pos p) + | Rconcat p -> Rconcat (pos p) + | Rconcat_ret p -> Rconcat_ret (pos p) + | Rlogic p -> Rlogic (pos p) + | Rlogic_ret p -> Rlogic_ret (pos p) + | Rbitwise p -> Rbitwise (pos p) + | Rbitwise_ret p -> Rbitwise_ret (pos p) + | Rstmt p -> Rstmt (pos p) + | Rno_return p -> Rno_return (pos p) + | Rno_return_async p -> Rno_return_async (pos p) + | Rret_fun_kind (p, k) -> Rret_fun_kind (pos p, k) + | Rhint p -> Rhint (pos p) + | Rnull_check p -> Rnull_check (pos p) + | Rnot_in_cstr p -> Rnot_in_cstr (pos p) + | Rthrow p -> Rthrow (pos p) + | Rplaceholder p -> Rplaceholder (pos p) + | Rattr p -> Rattr (pos p) + | Rxhp p -> Rxhp (pos p) + | Rret_div p -> Rret_div (pos p) + | Ryield_gen p -> Ryield_gen (pos p) + | Ryield_asyncgen p -> Ryield_asyncgen (pos p) + | Ryield_asyncnull p -> Ryield_asyncnull (pos p) + | Ryield_send p -> Ryield_send (pos p) + | Rlost_info (s, r1, p2) -> Rlost_info (s, reason r1, pos p2) + | Rcoerced (p1, p2, x) -> Rcoerced (pos p1, pos p2, x) + | Rformat (p1, s, r) -> Rformat (pos p1, s, reason r) + | Rclass_class (p, s) -> Rclass_class (pos p, s) + | Runknown_class p -> Runknown_class (pos p) + | Rdynamic_yield (p1, p2, s1, s2) -> Rdynamic_yield(pos p1, pos p2, s1, s2) + | Rmap_append p -> Rmap_append (pos p) + | Rvar_param p -> Rvar_param (pos p) + | Runpack_param p -> Runpack_param (pos p) + | Rinstantiate (r1,x,r2) -> Rinstantiate (reason r1, x, reason r2) + | Rarray_filter (p, r) -> Rarray_filter (pos p, reason r) + | Rtype_access (r1, x, r2) -> Rtype_access (reason r1, x, reason r2) + | Rexpr_dep_type (r, p, n) -> Rexpr_dep_type (reason r, pos p, n) + | Rnullsafe_op p -> Rnullsafe_op (pos p) + | Rtconst_no_cstr (p, s) -> Rtconst_no_cstr (pos p, s) + | Rused_as_map p -> Rused_as_map (pos p) + | Rused_as_shape p -> Rused_as_shape (pos p) + | Rpredicated (p, f) -> Rpredicated (pos p, f) + | Rinstanceof (p, f) -> Rinstanceof (pos p, f) +let string_id (p, x) = pos p, x + +let rec ty (p, x) = + reason p, ty_ x and ty_: decl ty_ -> decl ty_ = function | Tany @@ -103,7 +103,7 @@ module TraversePos(ImplementPos: sig val pos: Pos.t -> Pos.t end) = struct Taccess (ty root_ty, List.map ids string_id) | Tshape (fields_known, fdm) -> Tshape (shape_fields_known fields_known, - ShapeMap.map_and_rekey fdm shape_field_name ty) + ShapeFieldMap.map_and_rekey fdm shape_field_name ty) and ty_opt x = Option.map x ty diff --git a/hphp/hack/src/typing/type_mapper.ml b/hphp/hack/src/typing/type_mapper.ml index 16971ca6e43d0..f5bbef361a023 100644 --- a/hphp/hack/src/typing/type_mapper.ml +++ b/hphp/hack/src/typing/type_mapper.ml @@ -49,7 +49,11 @@ class type type_mapper_type = object method on_tclass : env -> Reason.t -> Nast.sid -> locl ty list -> result method on_tobject : env -> Reason.t -> result method on_tshape : - env -> Reason.t -> shape_fields_known -> locl ty Nast.ShapeMap.t -> result + env + -> Reason.t + -> shape_fields_known + -> locl shape_field_type Nast.ShapeMap.t + -> result method on_type : env -> locl ty -> result end @@ -179,7 +183,7 @@ class deep_type_mapper = object(this) let env, tyl = List.map_env env tyl this#on_type in env, (r, Tclass (x, tyl)) method! on_tshape env r fields_known fdm = - let env, fdm = Nast.ShapeMap.map_env this#on_type env fdm in + let env, fdm = ShapeFieldMap.map_env this#on_type env fdm in env, (r, Tshape (fields_known, fdm)) method private on_opt_type env x = match x with diff --git a/hphp/hack/src/typing/type_visitor.ml b/hphp/hack/src/typing/type_visitor.ml index 59093d3bb7336..9dffb9dbf77df 100644 --- a/hphp/hack/src/typing/type_visitor.ml +++ b/hphp/hack/src/typing/type_visitor.ml @@ -31,7 +31,11 @@ class type ['a] type_visitor_type = object method on_tunresolved : 'a -> Reason.t -> locl ty list -> 'a method on_tobject : 'a -> Reason.t -> 'a method on_tshape : - 'a -> Reason.t -> shape_fields_known -> 'b ty Nast.ShapeMap.t -> 'a + 'a + -> Reason.t + -> shape_fields_known + -> 'b shape_field_type Nast.ShapeMap.t + -> 'a method on_taccess : 'a -> Reason.t -> taccess_type -> 'a method on_tclass : 'a -> Reason.t -> Nast.sid -> locl ty list -> 'a method on_tarraykind : 'a -> Reason.t -> array_kind -> 'a @@ -77,9 +81,9 @@ class virtual ['a] type_visitor : ['a] type_visitor_type = object(this) method on_tunresolved acc _ tyl = List.fold_left tyl ~f:this#on_type ~init:acc method on_tobject acc _ = acc method on_tshape: type a. _ -> Reason.t -> shape_fields_known - -> a ty Nast.ShapeMap.t -> _ = + -> a shape_field_type Nast.ShapeMap.t -> _ = fun acc _ _ fdm -> - let f _ v acc = this#on_type acc v in + let f _ { sft_ty; _ } acc = this#on_type acc sft_ty in Nast.ShapeMap.fold f fdm acc method on_tclass acc _ _ tyl = List.fold_left tyl ~f:this#on_type ~init:acc diff --git a/hphp/hack/src/typing/typing.ml b/hphp/hack/src/typing/typing.ml index cf37b9919b1b2..f772df07c6dd8 100644 --- a/hphp/hack/src/typing/typing.ml +++ b/hphp/hack/src/typing/typing.ml @@ -163,7 +163,7 @@ let rec check_memoizable env param ty = | _, Tshape (_, fdm) -> ShapeMap.iter begin fun name _ -> match ShapeMap.get name fdm with - | Some ty -> check_memoizable env param ty + | Some { sft_ty; _ } -> check_memoizable env param sft_ty | None -> let ty_str = Typing_print.error (snd ty) in let msgl = Reason.to_string ("This is "^ty_str) (fst ty) in @@ -1621,9 +1621,12 @@ and expr_ (fun env e -> let env, te, ty = expr env e in env, (te,ty)) env fdm in let env, fdm = - ShapeMap.map_env - (fun env (_,ty) -> TUtils.unresolved env ty) - env tfdm in + let convert_expr_and_type_to_shape_field_type env (_, ty) = + let env, sft_ty = TUtils.unresolved env ty in + (* An expression evaluation always corresponds to a shape_field_type + with sft_optional = false. *) + env, { sft_optional = false; sft_ty } in + ShapeMap.map_env convert_expr_and_type_to_shape_field_type env tfdm in let env = check_shape_keys_validity env p (ShapeMap.keys fdm) in (* Fields are fully known, because this shape is constructed * using shape keyword and we know exactly what fields are set. *) @@ -2962,7 +2965,7 @@ and array_get is_lvalue p env ty1 e2 ty2 = Errors.undefined_field p (TUtils.get_printable_shape_field_name field); env, (Reason.Rwitness p, Terr) - | Some ty -> env, ty) + | Some { sft_ty; _ } -> env, sft_ty) ) | Toption _ -> Errors.null_container p diff --git a/hphp/hack/src/typing/typing_arrays.ml b/hphp/hack/src/typing/typing_arrays.ml index 37c3b056d3f18..c849c2d272a7b 100644 --- a/hphp/hack/src/typing/typing_arrays.ml +++ b/hphp/hack/src/typing/typing_arrays.ml @@ -203,7 +203,13 @@ let update_array_type p access_type ~lvar_assignment env ty = method! on_tshape env r fields_known fdm = match access_type with | AKshape_key field_name when lvar_assignment -> - let env, tv = Env.fresh_unresolved_type env in + let env, sft_ty = Env.fresh_unresolved_type env in + (* When we assign to a shape, like: + * + * $shape['field'] = // some type + * + * We want to infer the shape field as non-optional. *) + let tv = { sft_optional = false; sft_ty } in let fdm = ShapeMap.add field_name tv fdm in env, (Reason.Rwitness p, Tshape (fields_known, fdm)) | _ -> diff --git a/hphp/hack/src/typing/typing_defs.ml b/hphp/hack/src/typing/typing_defs.ml index a8b139a078da1..ebf614834efc2 100644 --- a/hphp/hack/src/typing/typing_defs.ml +++ b/hphp/hack/src/typing/typing_defs.ml @@ -30,6 +30,20 @@ type decl = private DeclPhase type locl = private LoclPhase type 'phase ty = Reason.t * 'phase ty_ + +(* A shape may specify whether or not fields are required. For example, consider + this typedef: + + type ShapeWithOptionalField = shape(?'a' => ?int); + + With this definition, the field 'a' may be unprovided in a shape. In this + case, the field 'a' would have sf_optional set to true. + *) +and 'phase shape_field_type = { + sft_optional : bool; + sft_ty : 'phase ty; +} + and _ ty_ = (*========== Following Types Exist Only in the Declared Phase ==========*) (* The late static bound type of a class *) @@ -108,7 +122,9 @@ and _ ty_ = (* Whether all fields of this shape are known, types of each of the * known arms. *) - | Tshape : shape_fields_known * ('phase ty Nast.ShapeMap.t) -> 'phase ty_ + | Tshape + : shape_fields_known * ('phase shape_field_type Nast.ShapeMap.t) + -> 'phase ty_ (*========== Below Are Types That Cannot Be Declared In User Code ==========*) @@ -493,6 +509,43 @@ module AbstractKind = struct String.concat "::" (dt::ids) end +module ShapeFieldMap = struct + include Nast.ShapeMap + + let map_and_rekey shape_map key_f value_f = + let f_over_shape_field_type ({ sft_ty; _ } as shape_field_type) = + { shape_field_type with sft_ty = value_f sft_ty } in + Nast.ShapeMap.map_and_rekey + shape_map + key_f + f_over_shape_field_type + + let map_env f env shape_map = + let f_over_shape_field_type env ({ sft_ty; _ } as shape_field_type) = + let env, sft_ty = f env sft_ty in + env, { shape_field_type with sft_ty } in + Nast.ShapeMap.map_env f_over_shape_field_type env shape_map + + let map f shape_map = map_and_rekey shape_map (fun x -> x) f + + let iter f shape_map = + let f_over_shape_field_type shape_map_key { sft_ty; _ } = + f shape_map_key sft_ty in + Nast.ShapeMap.iter f_over_shape_field_type shape_map + + let iter_values f = iter (fun _ -> f) +end + +module ShapeFieldList = struct + include Core.List + + let map_env env xs ~f = + let f_over_shape_field_type env ({ sft_ty; _ } as shape_field_type) = + let env, sft_ty = f env sft_ty in + env, { shape_field_type with sft_ty } in + Core.List.map_env env xs ~f:f_over_shape_field_type +end + (*****************************************************************************) (* Suggest mode *) (*****************************************************************************) diff --git a/hphp/hack/src/typing/typing_generic.ml b/hphp/hack/src/typing/typing_generic.ml index 300d3c6014e5c..36fe9d325f2f5 100644 --- a/hphp/hack/src/typing/typing_generic.ml +++ b/hphp/hack/src/typing/typing_generic.ml @@ -60,7 +60,7 @@ end = struct | Tunresolved tyl -> List.iter tyl ty | Tobject -> () | Tshape (_, fdm) -> - ShapeMap.iter (fun _ v -> ty v) fdm + ShapeFieldMap.iter (fun _ v -> ty v) fdm and ty_opt = function None -> () | Some x -> ty x diff --git a/hphp/hack/src/typing/typing_phase.ml b/hphp/hack/src/typing/typing_phase.ml index 3cc1ab92b40de..f03cde649430c 100644 --- a/hphp/hack/src/typing/typing_phase.ml +++ b/hphp/hack/src/typing/typing_phase.ml @@ -173,7 +173,7 @@ let rec localize_with_env ~ety_env env (dty: decl ty) = let env, root_ty = localize ~ety_env env root_ty in TUtils.expand_typeconst ety_env env r root_ty ids | r, Tshape (fields_known, tym) -> - let env, tym = ShapeMap.map_env (localize ~ety_env) env tym in + let env, tym = ShapeFieldMap.map_env (localize ~ety_env) env tym in env, (ety_env, (r, Tshape (fields_known, tym))) and localize ~ety_env env ty = diff --git a/hphp/hack/src/typing/typing_print.ml b/hphp/hack/src/typing/typing_print.ml index a87274d33a3a3..fbd9f9c3d4f40 100644 --- a/hphp/hack/src/typing/typing_print.ml +++ b/hphp/hack/src/typing/typing_print.ml @@ -245,13 +245,10 @@ module Full = struct | [x] -> f x | x :: rl -> f x; o s; list_sep o s f rl - let shape_map o fdm f = + let shape_map o fdm o_field = let cmp = (fun (k1, _) (k2, _) -> compare (Env.get_shape_field_name k1) (Env.get_shape_field_name k2)) in let fields = List.sort ~cmp (Nast.ShapeMap.elements fdm) in - let o_field = (fun (k, v) -> - o (Env.get_shape_field_name k); o " => "; f v;) - in (match fields with | [] -> () | f::l -> @@ -283,8 +280,14 @@ module Full = struct | Tarray (Some x, Some y) -> o "array<"; k x; o ", "; k y; o ">" | Tarraykind AKdarray (x, y) -> o "darray<"; k x; o ", "; k y; o ">" | Tarraykind (AKmap (x, y)) -> o "array<"; k x; o ", "; k y; o ">" - | Tarraykind (AKshape fdm) -> o "shape-like-array("; - shape_map o fdm (fun (_tk, tv) -> k tv); o ")" + | Tarraykind (AKshape fdm) -> + let o_field (shape_map_key, (_tk, tv)) = + o (Env.get_shape_field_name shape_map_key); + o " => "; + k tv in + o "shape-like-array("; + shape_map o fdm o_field; + o ")" | Tarraykind (AKtuple fields) -> o "tuple-like-array("; list k (List.rev (IMap.values fields)); o ")" | Tarray (None, Some _) -> assert false @@ -342,7 +345,27 @@ module Full = struct end end; o "("; - shape_map o fdm (fun t -> k t); + let optional_shape_field_enabled = + TypecheckerOptions.experimental_feature_enabled + (Env.get_options env) + TypecheckerOptions.experimental_optional_shape_field in + let o_field (shape_map_key, { sft_optional; sft_ty }) = + if optional_shape_field_enabled then + begin + o (Env.get_shape_field_name shape_map_key); + o " => shape("; + if sft_optional then o "optional => true, "; + o "type => "; + k sft_ty; + o ")" + end + else + begin + o (Env.get_shape_field_name shape_map_key); + o " => "; + k sft_ty + end in + shape_map o fdm o_field; o ")" and prim o x = diff --git a/hphp/hack/src/typing/typing_shapes.ml b/hphp/hack/src/typing/typing_shapes.ml index 74bb43a31cb1e..afbc6f7f92d2d 100644 --- a/hphp/hack/src/typing/typing_shapes.ml +++ b/hphp/hack/src/typing/typing_shapes.ml @@ -49,13 +49,22 @@ let idx env fty shape_ty field default = match TUtils.shape_field_name env (fst field) (snd field) with | None -> env, (Reason.Rwitness (fst field), Tany) | Some field_name -> + let optional_shape_field_enabled = + TypecheckerOptions.experimental_feature_enabled + (Env.get_options env) + TypecheckerOptions.experimental_optional_shape_field in + let fake_shape_field = + if optional_shape_field_enabled then + { sft_optional = true; sft_ty = res } + else + { sft_optional = false; sft_ty = (Reason.Rnone, Toption res) } in let fake_shape = ( (* Rnone because we don't want the fake shape to show up in messages about * field non existing. Errors.missing_optional_field filters them out *) Reason.Rnone, Tshape ( FieldsPartiallyKnown Nast.ShapeMap.empty, - Nast.ShapeMap.singleton field_name (Reason.Rnone, Toption res) + Nast.ShapeMap.singleton field_name fake_shape_field ) ) in let env = @@ -85,7 +94,8 @@ let to_array env shape_ty res = match fields_known with | FieldsFullyKnown -> let env, values = - List.map_env env (ShapeMap.values fdm) (Typing_utils.unresolved) in + ShapeFieldList.map_env + env (ShapeMap.values fdm) (Typing_utils.unresolved) in let keys = ShapeMap.keys fdm in let env, keys = List.map_env env keys begin fun env key -> let env, ty = match key with @@ -103,6 +113,7 @@ let to_array env shape_ty res = end in let env, key = Typing_arrays.array_type_list_to_single_type env keys in + let values = List.map ~f:(fun { sft_ty; _ } -> sft_ty) values in let env, value = Typing_arrays.array_type_list_to_single_type env values in env, (r, Tarraykind (AKmap (key, value))) diff --git a/hphp/hack/src/typing/typing_structure.ml b/hphp/hack/src/typing/typing_structure.ml index cb7893776e5f7..4eb9512594c20 100644 --- a/hphp/hack/src/typing/typing_structure.ml +++ b/hphp/hack/src/typing/typing_structure.ml @@ -55,29 +55,36 @@ let rec transform_shapemap ?(nullable = false) env ty shape = if is_unbound then (env, shape) else let is_generic = match snd ety with Tabstract (AKgeneric _, _) -> true | _ -> false in - ShapeMap.fold begin fun field field_ty (env, shape) -> + let transform_shape_field field { sft_ty; _ } (env, shape) = let open Ast in - match field, field_ty, TUtils.get_base_type env ety with + + (* Accumulates the provided type for this iteration of the fold, adding + it to the accumulation ShapeMap for the current field. Since the + field must have been explicitly set, we set sft_optional to true. *) + let acc_field_with_type sft_ty = + ShapeMap.add field { sft_optional = false; sft_ty } shape in + + match field, sft_ty, TUtils.get_base_type env ety with | SFlit (_, "nullable"), (_, Toption (fty)), _ when nullable -> - env, ShapeMap.add field fty shape + env, acc_field_with_type fty | SFlit (_, "nullable"), (_, Toption (fty)), (_, Toption _) -> - env, ShapeMap.add field fty shape + env, acc_field_with_type fty | SFlit (_, "classname"), (_, Toption (fty)), (_, (Tclass _ | Tabstract (AKenum _, _))) -> - env, ShapeMap.add field fty shape + env, acc_field_with_type fty | SFlit (_, "elem_types"), _, (r, Ttuple tyl) -> let env, tyl = List.map_env env tyl make_ts in - env, ShapeMap.add field (r, Ttuple tyl) shape + env, acc_field_with_type (r, Ttuple tyl) | SFlit (_, "param_types"), _, (r, (Tfun funty)) -> let tyl = List.map ~f:snd funty.ft_params in let env, tyl = List.map_env env tyl make_ts in - env, ShapeMap.add field (r, Ttuple tyl) shape + env, acc_field_with_type (r, Ttuple tyl) | SFlit (_, "return_type"), _, (r, Tfun funty) -> let env, ty = make_ts env funty.ft_ret in - env, ShapeMap.add field (r, Ttuple [ty]) shape + env, acc_field_with_type (r, Ttuple [ty]) | SFlit (_, "fields"), _, (r, Tshape (fk, fields)) -> - let env, fields = ShapeMap.map_env make_ts env fields in - env, ShapeMap.add field (r, Tshape (fk, fields)) shape + let env, fields = ShapeFieldMap.map_env make_ts env fields in + env, acc_field_with_type (r, Tshape (fk, fields)) (* For generics we cannot specialize the generic_types field. Consider: * * class C {} @@ -90,23 +97,23 @@ let rec transform_shapemap ?(nullable = false) env ty shape = * For test(TypeStructure) there will not be a generic_types field *) | SFlit (_, "generic_types"), _, _ when is_generic -> - env, ShapeMap.add field field_ty shape + env, acc_field_with_type sft_ty | SFlit (_, "generic_types"), _, (r, Tarraykind (AKvec ty)) when not is_generic -> let env, ty = make_ts env ty in - env, ShapeMap.add field (r, Ttuple [ty]) shape + env, acc_field_with_type (r, Ttuple [ty]) | SFlit (_, "generic_types"), _, (r, Tarraykind (AKmap (ty1, ty2))) when not is_generic -> let tyl = [ty1; ty2] in let env, tyl = List.map_env env tyl make_ts in - env, ShapeMap.add field (r, Ttuple tyl) shape + env, acc_field_with_type (r, Ttuple tyl) | SFlit (_, "generic_types"), _, (r, Tclass (_, tyl)) when List.length tyl > 0 -> let env, tyl = List.map_env env tyl make_ts in - env, ShapeMap.add field (r, Ttuple tyl) shape + env, acc_field_with_type (r, Ttuple tyl) | SFlit (_, ("kind" | "name" | "alias")), _, _ -> - env, ShapeMap.add field field_ty shape + env, acc_field_with_type sft_ty | _, _, _ -> - env, shape - end shape (env, ShapeMap.empty) + env, shape in + ShapeMap.fold transform_shape_field shape (env, ShapeMap.empty) diff --git a/hphp/hack/src/typing/typing_subtype.ml b/hphp/hack/src/typing/typing_subtype.ml index d7c79b2c3cbf6..2400e6105fe99 100644 --- a/hphp/hack/src/typing/typing_subtype.ml +++ b/hphp/hack/src/typing/typing_subtype.ml @@ -827,8 +827,27 @@ and sub_type_with_uenv env (uenv_sub, ty_sub) (uenv_super, ty_super) = ) | (r_sub, Tshape (fields_known_sub, fdm_sub)), (r_super, Tshape (fields_known_super, fdm_super)) -> + (** + * shape_field_type A <: shape_field_type B iff: + * 1. A is no more optional than B + * 2. A's type <: B.type + *) + let on_common_field + (env, acc) + name + { sft_optional = optional_super; sft_ty = ty_super } + { sft_optional = optional_sub; sft_ty = ty_sub } = + match optional_super, optional_sub with + | true, _ | false, false -> + sub_type env ty_sub ty_super, acc + | false, true -> + Errors.required_field_is_optional + (Reason.to_pos r_sub) + (Reason.to_pos r_super) + (Env.get_shape_field_name name); + env, acc in fst (TUtils.apply_shape - ~on_common_field:(fun (env, acc) _ x y -> sub_type env y x, acc) + ~on_common_field ~on_missing_optional_field:(fun acc _ _ -> acc) (env, None) (r_super, fields_known_super, fdm_super) diff --git a/hphp/hack/src/typing/typing_unify.ml b/hphp/hack/src/typing/typing_unify.ml index 65eeca2d9e68c..aeeeccc0b9945 100644 --- a/hphp/hack/src/typing/typing_unify.ml +++ b/hphp/hack/src/typing/typing_unify.ml @@ -315,11 +315,33 @@ and unify_ ?follow_bounds:(follow_bounds=true) env r1 ty1 r2 ty2 = | Tobject, Tclass _ | Tclass _, Tobject -> env, Tobject | Tshape (fields_known1, fdm1), Tshape (fields_known2, fdm2) -> - let on_common_field (env, acc) name ty1 ty2 = - let env, ty = unify env ty1 ty2 in - env, Nast.ShapeMap.add name ty acc in - let on_missing_optional_field (env, acc) name ty = - env, Nast.ShapeMap.add name ty acc in + (** + * shape_field_type A and shape_field_type B are unifiable iff: + * 1. A and B have the same optionality + * 2. A's type and B's type are unifiable + *) + let on_common_field + (env, acc) + name + { sft_optional = sft_optional1; sft_ty = ty1 } + { sft_optional = sft_optional2; sft_ty = ty2 } = + if sft_optional1 = sft_optional2 + then + let env, sft_ty = unify env ty1 ty2 in + let common_shape_field_type = { + sft_optional = sft_optional1; + sft_ty; + } in + env, Nast.ShapeMap.add name common_shape_field_type acc + else + let optional, required = if sft_optional1 then r1, r2 else r2, r1 in + Errors.required_field_is_optional + (Reason.to_pos optional) + (Reason.to_pos required) + (Env.get_shape_field_name name); + env, acc in + let on_missing_optional_field (env, acc) name missing_shape_field_type = + env, Nast.ShapeMap.add name missing_shape_field_type acc in (* We do it both directions to verify that no field is missing *) let res = Nast.ShapeMap.empty in let env, res = TUtils.apply_shape diff --git a/hphp/hack/src/typing/typing_unify_recursive.ml b/hphp/hack/src/typing/typing_unify_recursive.ml index bca7f4142d52c..901c30041dd46 100644 --- a/hphp/hack/src/typing/typing_unify_recursive.ml +++ b/hphp/hack/src/typing/typing_unify_recursive.ml @@ -50,7 +50,8 @@ let rec occurs env n rty = | Tabstract(ak,topt) -> occurs_ak env n ak || occurs_opt env n topt | Tarraykind ak -> occurs_array env n ak | Tfun ft -> occurs_ft env n ft - | Tshape(_,sm) -> Nast.ShapeMap.exists (fun _ t -> occurs env n t) sm + | Tshape(_,sm) -> + Nast.ShapeMap.exists (fun _ { sft_ty; _ } -> occurs env n sft_ty) sm and occurs_opt env n topt = match topt with | None -> false diff --git a/hphp/hack/src/typing/typing_utils.ml b/hphp/hack/src/typing/typing_utils.ml index 92bfd1762d5fd..cda03c2bfc4f4 100644 --- a/hphp/hack/src/typing/typing_utils.ml +++ b/hphp/hack/src/typing/typing_utils.ml @@ -57,6 +57,17 @@ let rec is_option env ty = List.exists tyl (is_option env) | _ -> false +let is_shape_field_optional env { sft_optional; sft_ty } = + let optional_shape_field_enabled = + TypecheckerOptions.experimental_feature_enabled + (Env.get_options env) + TypecheckerOptions.experimental_optional_shape_field in + + if optional_shape_field_enabled then + sft_optional + else + is_option env sft_ty + let is_class ty = match snd ty with | Tclass _ -> true | _ -> false @@ -204,15 +215,15 @@ let apply_shape ~on_common_field ~on_missing_optional_field (env, acc) end unset_fields1 | _ -> () end; - ShapeMap.fold begin fun name ty1 (env, acc) -> + ShapeMap.fold begin fun name shape_field_type_1 (env, acc) -> match ShapeMap.get name fdm2 with - | None when is_option env ty1 -> + | None when is_shape_field_optional env shape_field_type_1 -> let can_omit = match fields_known2 with | FieldsFullyKnown -> true | FieldsPartiallyKnown unset_fields -> ShapeMap.mem name unset_fields in if can_omit then - on_missing_optional_field (env, acc) name ty1 + on_missing_optional_field (env, acc) name shape_field_type_1 else let pos1 = Reason.to_pos r1 in let pos2 = Reason.to_pos r2 in @@ -224,8 +235,8 @@ let apply_shape ~on_common_field ~on_missing_optional_field (env, acc) let pos2 = Reason.to_pos r2 in Errors.missing_field pos2 pos1 (get_printable_shape_field_name name); (env, acc) - | Some ty2 -> - on_common_field (env, acc) name ty1 ty2 + | Some shape_field_type_2 -> + on_common_field (env, acc) name shape_field_type_1 shape_field_type_2 end fdm1 (env, acc) let shape_field_name_ env field = diff --git a/hphp/hack/src/typing/typing_variance.ml b/hphp/hack/src/typing/typing_variance.ml index a09a8eefcf0d7..27bb008d7a977 100644 --- a/hphp/hack/src/typing/typing_variance.ml +++ b/hphp/hack/src/typing/typing_variance.ml @@ -526,8 +526,8 @@ and type_ tcopt root variance env (reason, ty) = (* when we add type params to type consts might need to change *) | Taccess _ -> env | Tshape (_, ty_map) -> - Nast.ShapeMap.fold begin fun _field_name ty env -> - type_ tcopt root variance env ty + Nast.ShapeMap.fold begin fun _ { sft_ty; _ } env -> + type_ tcopt root variance env sft_ty end ty_map env (* `as` constraints on method type parameters must be contravariant diff --git a/hphp/hack/src/utils/errors/errors.ml b/hphp/hack/src/utils/errors/errors.ml index 7bf306c993dce..06e20457304a4 100644 --- a/hphp/hack/src/utils/errors/errors.ml +++ b/hphp/hack/src/utils/errors/errors.ml @@ -689,6 +689,7 @@ module Typing = struct let instanceof_always_true = 4160 (* DONT MODIFY!!!! *) let ambiguous_member = 4161 (* DONT MODIFY!!!! *) let instanceof_generic_classname = 4162 (* DONT MODIFY!!!! *) + let required_field_is_optional = 4163 (* DONT MODIFY!!!! *) (* EXTEND HERE WITH NEW VALUES IF NEEDED *) end @@ -2060,6 +2061,13 @@ let instanceof_generic_classname pos name = name ^ "' may be instantiated with a type such as \ 'C' that cannot be checked at runtime") +let required_field_is_optional pos1 pos2 name = + add_list Typing.required_field_is_optional + [ + pos1, "The field '"^name^"' is optional"; + pos2, "The field '"^name^"' is defined as required" + ] + (*****************************************************************************) (* Typing decl errors *) (*****************************************************************************) diff --git a/hphp/hack/src/utils/errors/errors_sig.ml b/hphp/hack/src/utils/errors/errors_sig.ml index de5ea73fed131..39a01ebea1f7f 100644 --- a/hphp/hack/src/utils/errors/errors_sig.ml +++ b/hphp/hack/src/utils/errors/errors_sig.ml @@ -356,4 +356,5 @@ module type S = sig val darray_not_supported : Pos.t -> unit val varray_not_supported : Pos.t -> unit val too_few_type_arguments : Pos.t -> unit + val required_field_is_optional : Pos.t -> Pos.t -> string -> unit end diff --git a/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_and_non_optional_types.php b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_and_non_optional_types.php new file mode 100644 index 0000000000000..8d2a7abf64dc2 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_and_non_optional_types.php @@ -0,0 +1,14 @@ + int, + 'b' => int, +); + +function foo(ShapeWithOptionalAndNonOptionalFields $argument): void {} + +function bar(): void { + foo(shape()); +} diff --git a/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_and_non_optional_types.php.exp b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_and_non_optional_types.php.exp new file mode 100644 index 0000000000000..907caa1d86256 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_and_non_optional_types.php.exp @@ -0,0 +1,6 @@ +File "shape_structural_subtyping_with_optional_and_non_optional_types.php", line 13, characters 7-11: +Invalid argument (Typing[4057]) +File "shape_structural_subtyping_with_optional_and_non_optional_types.php", line 13, characters 7-11: +The field 'b' is missing +File "shape_structural_subtyping_with_optional_and_non_optional_types.php", line 10, characters 14-50: +The field 'b' is defined diff --git a/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_and_non_optional_types.php.no_format b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_and_non_optional_types.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type.php b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type.php new file mode 100644 index 0000000000000..2f8e28faa977f --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type.php @@ -0,0 +1,13 @@ + int, + ?'b' => int, +); + +function foo(ShapeWithOptionalField $argument) : void {} + +function bar() : void { + foo(shape()); +} diff --git a/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type.php.exp b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type.php.exp new file mode 100644 index 0000000000000..4269126fcebc1 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type.php.exp @@ -0,0 +1 @@ +No errors diff --git a/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type.php.no_format b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type_and_extra_types.php b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type_and_extra_types.php new file mode 100644 index 0000000000000..c93ec5b1257c1 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type_and_extra_types.php @@ -0,0 +1,15 @@ + int, + ?'b' => int, +); + +function foo(ShapeWithOptionalField $argument) : void {} + +function bar() : void { + foo(shape('a' => 42, 'c' => "Hello")); +} diff --git a/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type_and_extra_types.php.exp b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type_and_extra_types.php.exp new file mode 100644 index 0000000000000..4269126fcebc1 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type_and_extra_types.php.exp @@ -0,0 +1 @@ +No errors diff --git a/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type_and_extra_types.php.no_format b/hphp/hack/test/typecheck/shape/shape_structural_subtyping_with_optional_type_and_extra_types.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape/shape_type_structure_with_optional_and_required_fields.php b/hphp/hack/test/typecheck/shape/shape_type_structure_with_optional_and_required_fields.php new file mode 100644 index 0000000000000..3afb487ec4842 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_type_structure_with_optional_and_required_fields.php @@ -0,0 +1,10 @@ + int, 'b' => bool); + +function test(TypeStructure $type_structure): void { + hh_show($type_structure['kind']); + hh_show($type_structure['alias']); + hh_show($type_structure['name']); + hh_show($type_structure['fields']); +} diff --git a/hphp/hack/test/typecheck/shape/shape_type_structure_with_optional_and_required_fields.php.exp b/hphp/hack/test/typecheck/shape/shape_type_structure_with_optional_and_required_fields.php.exp new file mode 100644 index 0000000000000..0103b38b0123c --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_type_structure_with_optional_and_required_fields.php.exp @@ -0,0 +1,9 @@ +File "shape_type_structure_with_optional_and_required_fields.php", line 6, characters 3-34: + int +File "shape_type_structure_with_optional_and_required_fields.php", line 7, characters 3-35: + ?string +File "shape_type_structure_with_optional_and_required_fields.php", line 8, characters 3-34: + ?string +File "shape_type_structure_with_optional_and_required_fields.php", line 9, characters 3-36: + shape(unset fields:)(a => shape(optional => true, type => TypeStructure), b => shape(type => TypeStructure)) +No errors diff --git a/hphp/hack/test/typecheck/shape/shape_type_structure_with_optional_and_required_fields.php.no_format b/hphp/hack/test/typecheck/shape/shape_type_structure_with_optional_and_required_fields.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape/shape_unification_allows_fields_that_are_both_optional.php b/hphp/hack/test/typecheck/shape/shape_unification_allows_fields_that_are_both_optional.php new file mode 100644 index 0000000000000..48bd05d2b5a1c --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_unification_allows_fields_that_are_both_optional.php @@ -0,0 +1,9 @@ + {} + +function take_shape(MyWrapper int)> $x): void {} + +function test(MyWrapper int)> $x): void { + take_shape($x); +} diff --git a/hphp/hack/test/typecheck/shape/shape_unification_allows_fields_that_are_both_optional.php.exp b/hphp/hack/test/typecheck/shape/shape_unification_allows_fields_that_are_both_optional.php.exp new file mode 100644 index 0000000000000..4269126fcebc1 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_unification_allows_fields_that_are_both_optional.php.exp @@ -0,0 +1 @@ +No errors diff --git a/hphp/hack/test/typecheck/shape/shape_unification_allows_fields_that_are_both_optional.php.no_format b/hphp/hack/test/typecheck/shape/shape_unification_allows_fields_that_are_both_optional.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php b/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php new file mode 100644 index 0000000000000..2a58c4f56095e --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php @@ -0,0 +1,9 @@ + {} + +function take_shape(MyWrapper int)> $x): void {} + +function test(MyWrapper int)> $x): void { + take_shape($x); +} diff --git a/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php.exp b/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php.exp new file mode 100644 index 0000000000000..1841e7f0dcb84 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php.exp @@ -0,0 +1,8 @@ +File "shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php", line 8, characters 14-15: +Invalid argument (Typing[4163]) +File "shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php", line 7, characters 25-29: +The field 'x' is optional +File "shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php", line 5, characters 31-35: +The field 'x' is defined as required +File "shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php", line 5, characters 31-35: +Considering that this type argument is invariant with respect to MyWrapper diff --git a/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php.no_format b/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_optional_field_where_required_field_is_consumed.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php b/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php new file mode 100644 index 0000000000000..f90a459b8c038 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php @@ -0,0 +1,9 @@ + {} + +function take_shape(MyWrapper int)> $x): void {} + +function test(MyWrapper int)> $x): void { + take_shape($x); +} diff --git a/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php.exp b/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php.exp new file mode 100644 index 0000000000000..8e8f22b8cbb42 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php.exp @@ -0,0 +1,8 @@ +File "shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php", line 8, characters 14-15: +Invalid argument (Typing[4163]) +File "shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php", line 5, characters 31-35: +The field 'x' is optional +File "shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php", line 7, characters 25-29: +The field 'x' is defined as required +File "shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php", line 5, characters 31-35: +Considering that this type argument is invariant with respect to MyWrapper diff --git a/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php.no_format b/hphp/hack/test/typecheck/shape/shape_unification_disallows_providing_required_field_where_optional_field_is_consumed.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape/shape_with_optional_supertype_field_and_required_subtype_field.php b/hphp/hack/test/typecheck/shape/shape_with_optional_supertype_field_and_required_subtype_field.php new file mode 100644 index 0000000000000..40a499c3901eb --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_with_optional_supertype_field_and_required_subtype_field.php @@ -0,0 +1,14 @@ + int); +type Subtype = shape('a' => int); + +function consumesSupertype(Supertype $argument): void {} + +function providesSubtype(): Subtype { + return shape('a' => 0); +} + +function test(): void { + consumesSupertype(providesSubtype()); +} diff --git a/hphp/hack/test/typecheck/shape/shape_with_optional_supertype_field_and_required_subtype_field.php.exp b/hphp/hack/test/typecheck/shape/shape_with_optional_supertype_field_and_required_subtype_field.php.exp new file mode 100644 index 0000000000000..4269126fcebc1 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_with_optional_supertype_field_and_required_subtype_field.php.exp @@ -0,0 +1 @@ +No errors diff --git a/hphp/hack/test/typecheck/shape/shape_with_optional_supertype_field_and_required_subtype_field.php.no_format b/hphp/hack/test/typecheck/shape/shape_with_optional_supertype_field_and_required_subtype_field.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape/shape_with_required_supertype_field_and_optional_subtype_field.php b/hphp/hack/test/typecheck/shape/shape_with_required_supertype_field_and_optional_subtype_field.php new file mode 100644 index 0000000000000..0ff67585bfe97 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_with_required_supertype_field_and_optional_subtype_field.php @@ -0,0 +1,14 @@ + int); +type Subtype = shape(?'a' => int); + +function consumesSupertype(Supertype $argument): void {} + +function providesSubtype(): Subtype { + return shape('a' => 0); +} + +function test(): void { + consumesSupertype(providesSubtype()); +} diff --git a/hphp/hack/test/typecheck/shape/shape_with_required_supertype_field_and_optional_subtype_field.php.exp b/hphp/hack/test/typecheck/shape/shape_with_required_supertype_field_and_optional_subtype_field.php.exp new file mode 100644 index 0000000000000..f325a10e23b97 --- /dev/null +++ b/hphp/hack/test/typecheck/shape/shape_with_required_supertype_field_and_optional_subtype_field.php.exp @@ -0,0 +1,6 @@ +File "shape_with_required_supertype_field_and_optional_subtype_field.php", line 13, characters 21-37: +Invalid argument (Typing[4163]) +File "shape_with_required_supertype_field_and_optional_subtype_field.php", line 8, characters 29-35: +The field 'a' is optional +File "shape_with_required_supertype_field_and_optional_subtype_field.php", line 6, characters 28-36: +The field 'a' is defined as required diff --git a/hphp/hack/test/typecheck/shape/shape_with_required_supertype_field_and_optional_subtype_field.php.no_format b/hphp/hack/test/typecheck/shape/shape_with_required_supertype_field_and_optional_subtype_field.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape14.php b/hphp/hack/test/typecheck/shape14.php index 8a08dcb3e8a71..8b6f2a4f5a30f 100644 --- a/hphp/hack/test/typecheck/shape14.php +++ b/hphp/hack/test/typecheck/shape14.php @@ -9,7 +9,7 @@ * */ -type myshape = shape('my_optional_field' => ?int); +type myshape = shape(?'my_optional_field' => int); function test(): myshape { return shape(); diff --git a/hphp/hack/test/typecheck/shape14.php.no_format b/hphp/hack/test/typecheck/shape14.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape24.php b/hphp/hack/test/typecheck/shape24.php index fb529e857d0c9..264172993f4d7 100644 --- a/hphp/hack/test/typecheck/shape24.php +++ b/hphp/hack/test/typecheck/shape24.php @@ -8,7 +8,7 @@ type t = shape( 'x' => int, - 'z' => ?bool, + ?'z' => bool, ); // Error: s declares only 'x', but at runtime it can also have a 'z' that diff --git a/hphp/hack/test/typecheck/shape24.php.no_format b/hphp/hack/test/typecheck/shape24.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape25.php b/hphp/hack/test/typecheck/shape25.php index e237fbe9da81e..a48f1bde33188 100644 --- a/hphp/hack/test/typecheck/shape25.php +++ b/hphp/hack/test/typecheck/shape25.php @@ -5,7 +5,7 @@ */ type t = shape( 'x' => int, - 'z' => ?bool, + ?'z' => bool, ); // No error: we are sure that there is no field 'z' in returned shape diff --git a/hphp/hack/test/typecheck/shape25.php.no_format b/hphp/hack/test/typecheck/shape25.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape26.php.exp b/hphp/hack/test/typecheck/shape26.php.exp index 3171cd9e5afc7..94ac8c7aeadf4 100644 --- a/hphp/hack/test/typecheck/shape26.php.exp +++ b/hphp/hack/test/typecheck/shape26.php.exp @@ -1,9 +1,9 @@ File "shape26.php", line 21, characters 3-13: - ^(shape(x => ^(int)) | shape(y => ^(string))) + ^(shape(x => shape(type => ^(int))) | shape(y => shape(type => ^(string)))) File "shape26.php", line 29, characters 3-13: - ^(shape(x => ^(string | int))) + ^(shape(x => shape(type => ^(string | int)))) File "shape26.php", line 37, characters 3-13: - ^(shape(unset fields:)(x => int) | shape(unset fields:)(x => int, y => ?string)) + ^(shape(unset fields:)(x => shape(type => int)) | shape(unset fields:)(x => shape(type => int), y => shape(type => ?string))) File "shape26.php", line 45, characters 3-13: - ^(shape(x => ^(int)) | shape(unset fields:)(x => int, y => ?string)) + ^(shape(x => shape(type => ^(int))) | shape(unset fields:)(x => shape(type => int), y => shape(type => ?string))) No errors diff --git a/hphp/hack/test/typecheck/shape27.php b/hphp/hack/test/typecheck/shape27.php index 166ebce58930f..fa6bde916e9c5 100644 --- a/hphp/hack/test/typecheck/shape27.php +++ b/hphp/hack/test/typecheck/shape27.php @@ -4,7 +4,7 @@ * Accessing fields/passing shapes to functions * doesn't change the "known fields" property */ -type s = shape('z' => ?int); +type s = shape(?'z' => int); function test(): s { $s = shape(); @@ -12,7 +12,7 @@ function test(): s { return $s; } -type t = shape('x' => ?int); +type t = shape(?'x' => int); function f(t $_): void {} diff --git a/hphp/hack/test/typecheck/shape27.php.no_format b/hphp/hack/test/typecheck/shape27.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape29.php.exp b/hphp/hack/test/typecheck/shape29.php.exp index 7b7d6e6ef12ec..2e50d17b3affe 100644 --- a/hphp/hack/test/typecheck/shape29.php.exp +++ b/hphp/hack/test/typecheck/shape29.php.exp @@ -1,5 +1,5 @@ File "shape29.php", line 6, characters 3-13: - shape(bar => ^(bool), baz => ^(string), foo => ^(int), xyz => ^(float)) + shape(bar => shape(type => ^(bool)), baz => shape(type => ^(string)), foo => shape(type => ^(int)), xyz => shape(type => ^(float))) File "shape29.php", line 7, characters 3-13: - shape(bar => ^(bool), baz => ^(string), foo => ^(int), xyz => ^(float)) + shape(bar => shape(type => ^(bool)), baz => shape(type => ^(string)), foo => shape(type => ^(int)), xyz => shape(type => ^(float))) No errors diff --git a/hphp/hack/test/typecheck/shape_idx3.php b/hphp/hack/test/typecheck/shape_idx3.php index 4090ab791cec5..92fefa0cb92f3 100644 --- a/hphp/hack/test/typecheck/shape_idx3.php +++ b/hphp/hack/test/typecheck/shape_idx3.php @@ -1,7 +1,7 @@ ?arraykey); -type my_shapeB = shape('x' => ?int); +type my_shapeA = shape(?'x' => arraykey); +type my_shapeB = shape(?'x' => int); function test( my_shapeA $s1, diff --git a/hphp/hack/test/typecheck/shape_idx3.php.no_format b/hphp/hack/test/typecheck/shape_idx3.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape_modified6.php b/hphp/hack/test/typecheck/shape_modified6.php index 80be7817b5481..1b6a59dbf8dd0 100644 --- a/hphp/hack/test/typecheck/shape_modified6.php +++ b/hphp/hack/test/typecheck/shape_modified6.php @@ -11,7 +11,7 @@ type my_shape = shape( 'x' => int, - 'y' => ?bool, + ?'y' => bool, ); function foo(bool $cond): my_shape { diff --git a/hphp/hack/test/typecheck/shape_modified6.php.no_format b/hphp/hack/test/typecheck/shape_modified6.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/shape_remove_key6.php b/hphp/hack/test/typecheck/shape_remove_key6.php index 7fdda6a9581a3..9df2016a0f7b5 100644 --- a/hphp/hack/test/typecheck/shape_remove_key6.php +++ b/hphp/hack/test/typecheck/shape_remove_key6.php @@ -8,7 +8,7 @@ type t = shape( 'x' => int, - 'z' => ?bool, + ?'z' => bool, ); function test(s $s): t { diff --git a/hphp/hack/test/typecheck/shape_remove_key6.php.no_format b/hphp/hack/test/typecheck/shape_remove_key6.php.no_format new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/hphp/hack/test/typecheck/type_structure/generic_type_structure.php.exp b/hphp/hack/test/typecheck/type_structure/generic_type_structure.php.exp index f7553ba7c9ef1..84462f5e18e4f 100644 --- a/hphp/hack/test/typecheck/type_structure/generic_type_structure.php.exp +++ b/hphp/hack/test/typecheck/type_structure/generic_type_structure.php.exp @@ -1,7 +1,7 @@ File "generic_type_structure.php", line 23, characters 3-15: TypeStructure<^(int)> File "generic_type_structure.php", line 24, characters 3-15: - TypeStructure<^(shape(unset fields:)(x => ?int, y => (string, bool)))> + TypeStructure<^(shape(unset fields:)(x => shape(type => ?int), y => shape(type => (string, bool))))> File "generic_type_structure.php", line 25, characters 3-36: (TypeStructure<^bool>, TypeStructure<^string>) No errors diff --git a/hphp/hack/test/typecheck/type_structure/type_structure1.php.exp b/hphp/hack/test/typecheck/type_structure/type_structure1.php.exp index cefe5e14a0eb8..42ec982e27fb2 100644 --- a/hphp/hack/test/typecheck/type_structure/type_structure1.php.exp +++ b/hphp/hack/test/typecheck/type_structure/type_structure1.php.exp @@ -23,7 +23,7 @@ File "type_structure1.php", line 38, characters 5-48: File "type_structure1.php", line 39, characters 5-68: classname> File "type_structure1.php", line 42, characters 5-17: - shape(unset fields:)(bar => TypeStructure (num, num))>, foo => TypeStructure) + shape(unset fields:)(bar => shape(type => TypeStructure shape(type => (num, num)))>), foo => shape(type => TypeStructure)) File "type_structure1.php", line 43, characters 5-24: TypeStructure File "type_structure1.php", line 44, characters 5-56: