2 * Copyright (c) 2018, Facebook, Inc.
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the "hack" directory of this source tree.
12 module Cls
= Decl_provider.Class
13 module SN
= Naming_special_names
14 module MakeType
= Typing_make_type
15 module Reason
= Typing_reason
19 | Mut
[@deriving show
]
21 let readonly_kind_to_rty = function
22 | Some
Ast_defs.Readonly
-> Readonly
25 let rty_to_str = function
26 | Readonly
-> "readonly"
29 let pp_rty fmt rty
= Format.fprintf fmt
"%s" (rty_to_str rty
)
31 (* Returns true if rty_sub is a subtype of rty_sup.
32 TODO: Later, we'll have to consider the regular type as well, for example
33 we could allow readonly int as equivalent to an int for devX purposes.
34 This would require TIC to handle correctly, though. *)
35 let subtype_rty rty_sub rty_sup
=
36 match (rty_sub
, rty_sup
) with
37 | (Readonly
, Mut
) -> false
40 let param_to_rty param
=
41 if Typing_defs.get_fp_readonly param
then
46 let rec grab_class_elts_from_ty ~static ?
(seen
= SSet.empty
) env ty prop_id
=
47 let open Typing_defs
in
48 (* Given a list of types, find recurse on the first type that
49 has the property and return the result *)
50 let find_first_in_list ~seen tyl
=
53 match grab_class_elts_from_ty ~static ~seen env ty prop_id
with
58 match get_node ty
with
59 | Tclass
(id
, _exact
, _args
) ->
60 let provider_ctx = Tast_env.get_ctx env
in
61 let class_decl = Decl_provider.get_class
provider_ctx (snd id
) in
62 (match class_decl with
66 Cls.get_sprop
class_decl (snd prop_id
)
68 Cls.get_prop
class_decl (snd prop_id
)
72 (* Accessing a property off of an intersection type
73 should involve exactly one kind of readonlyness, since for
74 the intersection type to exist, the property must be related
75 by some subtyping relationship anyways, and property readonlyness
76 is invariant. Thus we just grab the first one from the list where the prop exists. *)
77 | Tintersection
[] -> []
78 | Tintersection tyl
->
79 find_first_in_list ~seen tyl
|> Option.value ~default
:[]
80 (* A union type is more interesting, where we must return all possible cases
81 and be conservative in our use case. *)
84 ~f
:(fun ty
-> grab_class_elts_from_ty ~static ~seen env ty prop_id
)
86 (* Generic types can be treated similarly to an intersection type
87 where we find the first prop that works from the upper bounds *)
88 | Tgeneric
(name
, tyargs
) ->
89 (* Avoid circular generics with a set *)
90 if SSet.mem name seen
then
93 let new_seen = SSet.add name seen
in
94 let upper_bounds = Tast_env.get_upper_bounds env name tyargs
in
95 find_first_in_list ~seen
:new_seen (Typing_set.elements
upper_bounds)
96 |> Option.value ~default
:[]
97 | Tdependent
(_
, ty
) ->
98 (* Dependent types have an upper bound that's a class or generic *)
99 grab_class_elts_from_ty ~static ~seen env ty prop_id
101 (* If it's nullable, take the *)
102 grab_class_elts_from_ty ~static ~seen env ty prop_id
105 (* Return a list of possible static prop elts given a class_get expression *)
106 let get_static_prop_elts env class_id get
=
107 let (ty
, _
, _
) = class_id
in
109 | CGstring prop_id
-> grab_class_elts_from_ty ~static
:true env ty prop_id
110 (* An expression is dynamic, so there's no way to tell the type generally *)
113 (* Return a list of possible prop elts given an obj get expression *)
114 let get_prop_elts env obj get
=
115 let (env
, ty
) = Tast_env.expand_type env
(Tast.get_type obj
) in
117 | (_
, _
, Id prop_id
) -> grab_class_elts_from_ty ~static
:false env ty prop_id
118 (* TODO: Handle more complex cases *)
121 let rec ty_expr env
((_
, _
, expr_
) : Tast.expr
) : rty
=
123 | ReadonlyExpr _
-> Readonly
124 (* Obj_get, array_get, and class_get are here for better error messages when used as lval *)
125 | Obj_get
(e1
, e2
, _
, Is_prop
) ->
126 (match ty_expr env e1
with
127 | Readonly
-> Readonly
129 (* In the mut case, we need to check if the property is marked readonly *)
130 let prop_elts = get_prop_elts env e1 e2
in
132 List.find ~f
:Typing_defs.get_ce_readonly_prop
prop_elts
134 Option.value_map
readonly_prop ~default
:Mut ~f
:(fun _
-> Readonly
))
135 | Class_get
(class_id
, expr
, Is_prop
) ->
136 (* If any of the static props could be readonly, treat the expression as readonly *)
137 let class_elts = get_static_prop_elts env class_id expr
in
138 (* Note that the empty list case (when the prop doesn't exist) returns Mut *)
139 if List.exists
class_elts ~f
:Typing_defs.get_ce_readonly_prop
then
143 | Array_get
(array
, _
) -> ty_expr env array
146 let is_value_collection_ty env ty
=
147 let mixed = MakeType.mixed Reason.none
in
148 let env = Tast_env.tast_env_as_typing_env
env in
149 let hackarray = MakeType.any_array
Reason.none
mixed mixed in
150 (* Subtype against an empty open shape (shape(...)) *)
154 Typing_defs.Open_shape
155 Typing_defs.TShapeMap.empty
157 Typing_utils.is_sub_type
env ty
hackarray
158 || Typing_utils.is_sub_type
env ty
shape
160 (* Check if type is safe to convert from readonly to mut
161 TODO(readonly): Update to include more complex types. *)
162 let rec is_safe_mut_ty env (seen
: SSet.t
) ty
=
163 let open Typing_defs_core
in
164 let (env, ty
) = Tast_env.expand_type
env ty
in
165 match get_node ty
with
166 (* Allow all primitive types *)
168 (* Open shapes can technically have objects in them, but as long as the current fields don't have objects in them
169 we will allow you to call the function. Note that the function fails at runtime if any shape fields are objects. *)
170 | Tshape
(_
, fields
) ->
171 TShapeMap.for_all
(fun _k v
-> is_safe_mut_ty env seen v
.sft_ty
) fields
172 (* If it's a Tclass it's an array type by is_value_collection *)
173 | Tintersection tyl
-> List.exists tyl ~f
:(fun l
-> is_safe_mut_ty env seen l
)
174 (* Only error if there isn't a type that it could be that's primitive *)
175 | Tunion tyl
-> List.exists tyl ~f
:(fun l
-> is_safe_mut_ty env seen l
)
176 | Ttuple tyl
-> List.for_all tyl ~f
:(fun l
-> is_safe_mut_ty env seen l
)
177 | Tdependent
(_
, upper
) ->
178 (* check upper bounds *)
179 is_safe_mut_ty env seen upper
180 | Tclass
(_
, _
, tyl
) when is_value_collection_ty env ty
->
181 List.for_all tyl ~f
:(fun l
-> is_safe_mut_ty env seen l
)
182 | Tgeneric
(name
, tyargs
) ->
183 (* Avoid circular generics with a set *)
184 if SSet.mem name seen
then
187 let new_seen = SSet.add name seen
in
188 let upper_bounds = Tast_env.get_upper_bounds
env name tyargs
in
189 Typing_set.exists
(fun l
-> is_safe_mut_ty env new_seen l
) upper_bounds
191 (* Otherwise, check if there's any primitive type it could be *)
192 let env = Tast_env.tast_env_as_typing_env
env in
193 let primitive_types =
195 MakeType.bool Reason.none
;
196 MakeType.int Reason.none
;
197 MakeType.arraykey
Reason.none
;
198 MakeType.string Reason.none
;
199 MakeType.float Reason.none
;
200 MakeType.num
Reason.none
;
201 (* Keysets only contain arraykeys so if they're readonly its safe to remove *)
202 MakeType.keyset
Reason.none
(MakeType.arraykey
Reason.none
);
203 (* We don't put null here because we want to exclude ?Foo.
204 as_mut(null) itself is allowed by the Tprim above*)
207 let null = MakeType.null Reason.none
in
208 (* Make sure that a primitive *could* be this type by intersecting all primitives and subtyping. *)
209 let intersection = MakeType.intersection Reason.none
primitive_types in
210 let union = MakeType.union Reason.none
(null :: primitive_types) in
211 Typing_utils.is_sub_type
env intersection ty
212 || Typing_utils.is_sub_type
env ty
union
214 (* Check that function calls which return readonly are wrapped in readonly *)
215 let check_readonly_return_call pos caller_ty is_readonly
=
219 let open Typing_defs
in
220 match get_node caller_ty
with
221 | Tfun fty
when get_ft_returns_readonly fty
->
222 Errors.explicit_readonly_cast
225 (Typing_defs.get_pos caller_ty
)
228 let check_readonly_property env obj get obj_ro
=
229 let open Typing_defs
in
230 let prop_elts = get_prop_elts env obj get
in
231 (* If there's any property in the list of possible properties that could be readonly,
232 it must be explicitly cast to readonly *)
233 let readonly_prop = List.find ~f
:get_ce_readonly_prop
prop_elts in
234 match (readonly_prop, obj_ro
) with
236 Errors.explicit_readonly_cast
238 (Tast.get_position get
)
239 (Lazy.force elt
.ce_pos
)
242 let check_static_readonly_property pos
env (class_
: Tast.class_id
) get obj_ro
=
243 let prop_elts = get_static_prop_elts env class_ get
in
244 (* If there's any property in the list of possible properties that could be readonly,
245 it must be explicitly cast to readonly *)
246 let readonly_prop = List.find ~f
:Typing_defs.get_ce_readonly_prop
prop_elts in
247 match (readonly_prop, obj_ro
) with
248 | (Some elt
, Mut
) when Typing_defs.get_ce_readonly_prop elt
->
249 Errors.explicit_readonly_cast
252 (Lazy.force elt
.Typing_defs.ce_pos
)
255 let is_method_caller (caller
: Tast.expr
) =
257 | (_
, _
, ReadonlyExpr
(_
, _
, Obj_get
(_
, _
, _
, Is_method
)))
258 | (_
, _
, Obj_get
(_
, _
, _
, Is_method
)) ->
262 let is_special_builtin = function
263 (* none of these functions require readonly checks, and can take in readonly values safely *)
269 | "hphp_array_idx" ->
273 let rec assign env lval rval
=
274 (* Check that we're assigning a readonly value to a readonly property *)
275 let check_ro_prop_assignment prop_elts =
277 List.find ~f
:(fun r
-> not
(Typing_defs.get_ce_readonly_prop r
)) prop_elts
279 match mutable_prop with
280 | Some elt
when not
(Typing_defs.get_ce_readonly_prop elt
) ->
281 Errors.readonly_mismatch
282 "Invalid property assignment"
283 (Tast.get_position lval
)
286 ( Tast.get_position rval
|> Pos_or_decl.of_raw_pos
,
287 "This expression is readonly" );
291 ( Lazy.force elt
.Typing_defs.ce_pos
,
292 "But it's being assigned to a mutable property" );
297 (* List assignment *)
298 | (_
, _
, List exprs
) -> List.iter exprs ~f
:(fun lval
-> assign env lval rval
)
299 | (_
, _
, Array_get
(array
, _
)) ->
301 match (ty_expr env array
, ty_expr env rval
) with
302 | (Readonly
, _
) when is_value_collection_ty env (Tast.get_type array
) ->
303 (* In the case of (expr)[0] = rvalue, where expr is a value collection like vec,
304 we need to check assignment recursively because ($x->prop)[0] is only valid if $x is mutable and prop is readonly. *)
306 | (_
, _
, Array_get _
)
307 | (_
, _
, Obj_get _
) ->
308 assign env array rval
311 Errors.readonly_mismatch
312 "Invalid collection modification"
313 (Tast.get_position lval
)
316 ( Tast.get_position rval
|> Pos_or_decl.of_raw_pos
,
317 "This expression is readonly" );
321 ( Tast.get_position array
|> Pos_or_decl.of_raw_pos
,
322 "But this value is mutable" );
324 | (Readonly
, _
) -> Errors.readonly_modified
(Tast.get_position array
)
327 | (_
, _
, Class_get
(id
, expr
, Is_prop
)) ->
328 (match ty_expr env rval
with
330 let prop_elts = get_static_prop_elts env id expr
in
331 check_ro_prop_assignment prop_elts
333 | (_
, _
, Obj_get
(obj
, get
, _
, Is_prop
)) ->
334 (* Here to check for nested property accesses that are accessing readonly values *)
336 match ty_expr env obj
with
337 | Readonly
-> Errors.readonly_modified
(Tast.get_position obj
)
340 (match ty_expr env rval
with
342 let prop_elts = get_prop_elts env obj get
in
343 (* If there's a mutable prop, then there's a chance we're assigning to one *)
344 check_ro_prop_assignment prop_elts
346 (* TODO: make this exhaustive *)
349 (* Method call invocation *)
350 let method_call caller
=
351 let open Typing_defs
in
353 (* Readonly call checks *)
354 | (ty
, _
, ReadonlyExpr
(_
, _
, Obj_get
(e1
, _
, _
, Is_method
))) ->
355 (match get_node ty
with
356 | Tfun fty
when not
(get_ft_readonly_this fty
) ->
357 Errors.readonly_method_call
(Tast.get_position e1
) (get_pos ty
)
361 let check_special_function env caller args
=
362 match (caller
, args
) with
363 | ((_
, _
, Id
(pos
, x
)), [(_
, arg
)])
364 when String.equal
(Utils.strip_ns x
) (Utils.strip_ns
SN.Readonly.as_mut
) ->
365 let arg_ty = Tast.get_type arg
in
366 if not
(is_safe_mut_ty env SSet.empty
arg_ty) then
367 Errors.readonly_invalid_as_mut pos
372 (* Checks related to calling a function or method
373 is_readonly is true when the call is allowed to return readonly
380 (caller_ty
: Tast.ty
)
382 (args
: (Ast_defs.param_kind
* Tast.expr
) list
)
383 (unpacked_arg
: Tast.expr
option) =
384 let open Typing_defs
in
385 let (env, caller_ty
) = Tast_env.expand_type
env caller_ty
in
386 let check_readonly_closure caller_ty caller_rty
=
387 match (get_node caller_ty
, caller_rty
) with
388 | (Tfun fty
, Readonly
)
389 when (not
(get_ft_readonly_this fty
)) && not
method_call ->
390 (* Get the position of why this function is its current type (usually a typehint) *)
391 let reason = get_reason caller_ty
in
392 let f_pos = Reason.to_pos
(get_reason caller_ty
) in
395 (* If we got this function from a typehint, we suggest marking the function (readonly function) *)
396 | Typing_reason.Rhint _
->
398 Typing_defs_flags.(set_bit ft_flags_readonly_this
true fty
.ft_flags
)
400 let readonly_fty = Tfun
{ fty
with ft_flags
= new_flags } in
401 let suggested_fty = mk
(reason, readonly_fty) in
402 let suggested_fty_str = Tast_env.print_ty
env suggested_fty in
403 "annotate this typehint as a " ^
suggested_fty_str
404 (* Otherwise, it's likely from a Rwitness, but we suggest declaring it as readonly *)
405 | _
-> "declaring this as a `readonly` function"
407 Errors.readonly_closure_call pos
f_pos suggestion
410 (* Checks a single arg against a parameter *)
411 let check_arg param
(_
, arg
) =
412 let param_rty = param_to_rty param
in
413 let arg_rty = ty_expr env arg
in
414 if not
(subtype_rty arg_rty param_rty) then
415 Errors.readonly_mismatch
417 (Tast.get_position arg
)
420 ( Tast.get_position arg
|> Pos_or_decl.of_raw_pos
,
421 "This expression is " ^
rty_to_str arg_rty );
426 "It is incompatible with this parameter, which is "
427 ^
rty_to_str param_rty );
431 (* Check that readonly arguments match their parameters *)
432 let check_args caller_ty args unpacked_arg
=
433 match get_node caller_ty
with
437 |> Option.map ~f
:(fun e
-> (Ast_defs.Pnormal
, e
))
440 let args = args @ unpacked_rty in
441 let rec check args params
=
442 match (args, params
) with
443 | (x1
:: args1
, x2
:: params2
) ->
446 (* If either is empty, it's either a type error already or a default arg that's not filled in
447 either way, no need to check readonlyness *)
452 check args fty
.ft_params
455 check_readonly_closure caller_ty caller_rty
;
456 check_readonly_return_call pos caller_ty is_readonly
;
457 check_args caller_ty
args unpacked_arg
459 let caller_is_special_builtin caller
=
461 | (_
, _
, Id
(_
, name
)) when is_special_builtin (Utils.strip_ns name
) -> true
466 inherit Tast_visitor.iter
as super
468 method! on_expr
env e
=
470 | (_
, _
, Binop
(Ast_defs.Eq _
, lval
, rval
)) ->
471 assign env lval rval
;
472 self#on_expr
env rval
473 | (_
, _
, ReadonlyExpr
(_
, _
, Call
(caller
, targs
, args, unpacked_arg
))) ->
475 (* Skip the recursive step into ReadonlyExpr to avoid erroring *)
476 self#on_Call
env caller targs
args unpacked_arg
478 if caller_is_special_builtin caller
then
483 ~
method_call:(is_method_caller caller
)
485 (Tast.get_position caller
)
486 (Tast.get_type caller
)
490 check_special_function env caller
args;
493 (* Non readonly calls *)
494 | (_
, _
, Call
(caller
, _
, args, unpacked_arg
)) ->
495 if caller_is_special_builtin caller
then
501 ~
method_call:(is_method_caller caller
)
502 (Tast.get_position caller
)
503 (Tast.get_type caller
)
507 check_special_function env caller
args;
510 | (_
, _
, ReadonlyExpr
(_
, _
, Obj_get
(obj
, get
, nullable
, is_prop_call
)))
512 (* Skip the recursive step into ReadonlyExpr to avoid erroring *)
513 self#on_Obj_get
env obj get nullable is_prop_call
514 | (_
, _
, ReadonlyExpr
(_
, _
, Class_get
(class_
, get
, x
))) ->
515 (* Skip the recursive step into ReadonlyExpr to avoid erroring *)
516 self#on_Class_get
env class_ get x
517 | (_
, _
, Obj_get
(obj
, get
, _nullable
, Is_prop
)) ->
518 check_readonly_property env obj get Mut
;
520 | (_
, pos
, Class_get
(class_
, get
, Is_prop
)) ->
521 check_static_readonly_property pos
env class_ get Mut
;
523 | (_
, pos
, New
(_
, _
, args, unpacked_arg
, constructor_fty
)) ->
524 (* Constructors never return readonly, so that specific check is irrelevant *)
532 (List.map ~f
:(fun e
-> (Ast_defs.Pnormal
, e
)) args)
535 | (_
, _
, Class_get _
)
537 | (_
, _
, ValCollection
(_
, _
, _
))
538 | (_
, _
, KeyValCollection
(_
, _
, _
))
541 | (_
, _
, Array_get
(_
, _
))
546 | (_
, _
, Cast
(_
, _
))
547 | (_
, _
, Unop
(_
, _
))
548 | (_
, _
, Pipe
(_
, _
, _
))
549 | (_
, _
, Eif
(_
, _
, _
))
551 | (_
, _
, As
(_
, _
, _
))
552 | (_
, _
, Upcast
(_
, _
))
553 | (_
, _
, Import
(_
, _
))
554 | (_
, _
, Lplaceholder _
)
555 | (_
, _
, Pair
(_
, _
, _
))
556 | (_
, _
, ReadonlyExpr _
)
558 | (_
, _
, ExpressionTree _
)
561 (* Neither this nor any of the *_id expressions call the function *)
562 | (_
, _
, Method_caller
(_
, _
))
563 | (_
, _
, Smethod_id
(_
, _
))
565 | (_
, _
, Method_id _
)
566 | (_
, _
, FunctionPointer _
)
575 | (_
, _
, EnumClassLabel _
)
576 | (_
, _
, ET_Splice _
)
580 | (_
, _
, Dollardollar _
)
583 | (_
, _
, Collection
(_
, _
, _
))
584 | (_
, _
, Class_const _
)
586 | (_
, _
, PrefixedString _
)
593 inherit Tast_visitor.handler_base
595 (* Ref updated before every function def *)
596 val fun_has_readonly
= ref false
598 method! at_method_
env m
=
599 let env = Tast_env.restore_method_env
env m
in
600 if Tast_env.fun_has_readonly
env then (
601 fun_has_readonly
:= true;
602 check#on_method_
env m
604 fun_has_readonly
:= false;
608 method! at_fun_def
env f
=
609 let env = Tast_env.restore_fun_env
env f
.fd_fun
in
610 if Tast_env.fun_has_readonly
env then (
611 fun_has_readonly
:= true;
612 check#on_fun_def
env f
614 fun_has_readonly
:= false;
619 The following error checks are ones that need to run even if
620 readonly analysis is not enabled by the file attribute.
622 method! at_Call _env caller _tal _el _unpacked_element
=
623 (* this check is already handled by the readonly analysis,
624 which handles cases when there's a readonly keyword *)
625 if !fun_has_readonly
then
628 let caller_pos = Tast.get_position caller
in
629 let caller_ty = Tast.get_type caller
in
630 check_readonly_return_call caller_pos caller_ty false
632 method! at_expr
env e
=
633 (* this check is already handled by the readonly analysis,
634 which handles cases when there's a readonly keyword *)
636 if !fun_has_readonly
then
641 let val_kind = Tast_env.get_val_kind
env in
642 match (e
, val_kind) with
643 | ((_
, _
, Binop
(Ast_defs.Eq _
, lval
, rval
)), _
) ->
644 (* Check property assignments to make sure they're safe *)
646 (* Assume obj is mutable here since you can't have a readonly thing
647 without readonly keyword/analysis *)
648 (* Only check this for rvalues, not lvalues *)
649 | ((_
, _
, Obj_get
(obj
, get
, _
, Is_prop
)), Typing_defs.Other
) ->
650 check_readonly_property env obj get Mut
651 | ((_
, pos
, Class_get
(class_id
, get
, Is_prop
)), Typing_defs.Other
) ->
652 check_static_readonly_property pos
env class_id get Mut