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
17 | Mut
[@deriving show
]
21 (* whether the method/function returns readonly, and a Pos.t for error messages *)
22 ret_ty
: (rty
* Pos.t
) option;
23 (* Whether $this is readonly and a Pos.t for error messages *)
24 this_ty
: (rty
* Pos.t
) option;
27 let empty_ctx = { lenv
= SMap.empty
; ret_ty
= None
; this_ty
= None
}
29 let readonly_kind_to_rty = function
30 | Some
Ast_defs.Readonly
-> Readonly
33 let rty_to_str = function
34 | Readonly
-> "readonly"
37 let has_const_attribute user_attributes
=
38 List.exists user_attributes ~f
:(fun ua
->
41 Naming_special_names.UserAttributes.uaConstFun
)
43 let pp_rty fmt rty
= Format.fprintf fmt
"%s" (rty_to_str rty
)
45 (* Debugging tool for printing the local environment. Not actually called in code *)
46 let pp_lenv lenv
= SMap.show
pp_rty lenv
48 let lenv_from_params (params
: Tast.fun_param list
) user_attributes
: rty
SMap.t
50 let result = SMap.empty
in
51 let constfun = has_const_attribute user_attributes
in
59 Some
Ast_defs.Readonly
65 let get_local lenv id
=
66 match SMap.find_opt id lenv
with
70 (* Returns true if rty_sub is a subtype of rty_sup.
71 TODO: Later, we'll have to consider the regular type as well, for example
72 we could allow readonly int as equivalent to an int for devX purposes *)
73 let subtype_rty rty_sub rty_sup
=
74 match (rty_sub
, rty_sup
) with
75 | (Readonly
, Mut
) -> false
78 let param_to_rty param
=
79 if Typing_defs.get_fp_readonly param
then
86 inherit Tast_visitor.iter
as super
88 val mutable ctx
: ctx
= empty_ctx
90 method ty_expr
(e
: Tast.expr
) : rty
=
92 | ReadonlyExpr _
-> Readonly
94 (match ctx
.this_ty
with
98 let varname = Local_id.to_string lid
in
99 get_local ctx
.lenv
varname
100 (* If you have a bunch of property accesses in a row, i.e. $x->foo->bar->baz,
101 ty_expr will take linear time, and the full check may take O(n^2) time
102 if we recurse on expressions in the visitor. We expect this to generally
103 be quite small, though. *)
104 | Obj_get
(e1
, _
, _
, _
) -> self#ty_expr e1
105 | Await e
-> self#ty_expr e
106 (* $array[$x] access *)
107 | Array_get
(e
, Some _
) -> self#ty_expr e
108 (* This is only valid as an lval *)
109 | Array_get
(_
, None
) -> Mut
110 | Class_get _
-> (* TODO: static prop access*) Mut
115 (* All calls return mut by default, unless they are wrapped in a readonly expression *)
117 Mut
(* TODO: yield is a statement, really, not an expression. *)
119 Mut
(* Only appears as an lvalue; relevant in assign but not here *)
122 Mut
(* Unop only works on value types, so they can be mutable *)
123 (* All binary operators are either assignments or primitive binops, which are all value types *)
125 (* I think the right side is always a function call so this could be Mut *)
126 | Pipe
(_
, _left
, right
) -> self#ty_expr right
127 | KeyValCollection
(_
, _
, fl
) ->
129 List.exists fl ~f
:(fun (_
, value) ->
130 match self#ty_expr
value with
137 | ValCollection
(_
, _
, el
) ->
139 List.exists el ~f
:(fun e
->
140 match self#ty_expr e
with
147 | Eif
(_
, Some e1
, e2
) ->
148 (* Ternaries are readonly if either side is readonly *)
149 (match (self#ty_expr e1
, self#ty_expr e2
) with
154 | Eif
(_
, None
, e2
) -> self#ty_expr e2
155 | As
(expr
, _
, _
) -> self#ty_expr expr
156 | Is _
-> Mut
(* Booleans are value types *)
157 | Pair
(_
, e1
, e2
) ->
158 (match (self#ty_expr e1
, self#ty_expr e2
) with
163 | New _
-> Mut
(* All constructors are mutable by default *)
164 (* Things that don't appear in function bodies generally *)
168 | Lplaceholder _
-> Mut
169 (* Cloning something should always result in a mutable version of it *)
171 (* These are all value types without restrictions on mutability *)
195 | Collection
(_
, _
, _
)
197 | PrefixedString _
->
199 (* Disable formatting here so I can fit all of the above in one line *)
200 | Class_const _
-> Mut
202 method assign env lval rval
=
204 | (_
, Array_get
(array
, _
)) ->
205 (* TODO: appending to readonly value types is technically allowed *)
207 match self#ty_expr array
with
208 | Readonly
-> Errors.readonly_modified
(Tast.get_position array
)
211 | (_
, Obj_get
(obj
, get
, _
, _
)) ->
213 match self#ty_expr obj
with
214 | Readonly
-> Errors.readonly_modified
(Tast.get_position obj
)
217 let prop_elt = self#get_prop_elt env obj get
in
218 (match (prop_elt, self#ty_expr rval
) with
219 | (Some elt
, Readonly
) when not
(Typing_defs.get_ce_readonly_prop elt
)
221 Errors.readonly_mismatch
222 "Invalid property assignment"
223 (Tast.get_position lval
)
225 [(Tast.get_position rval
, "This expression is readonly")]
228 ( Lazy.force elt
.Typing_defs.ce_pos
,
229 "But it's being assigned to a mutable property" );
232 | (_
, Lvar
(_
, lid
)) ->
233 let var_ro_opt = SMap.find_opt
(Local_id.to_string lid
) ctx
.lenv
in
235 match (var_ro_opt, self#ty_expr rval
) with
236 | (Some Readonly
, Mut
) ->
237 Errors.var_readonly_mismatch
238 (Tast.get_position lval
)
240 (Tast.get_position rval
)
242 | (Some Mut
, Readonly
) ->
243 Errors.var_readonly_mismatch
244 (Tast.get_position lval
)
246 (Tast.get_position rval
)
249 (* If it's a new assignment, add to the lenv *)
250 let new_lenv = SMap.add
(Local_id.to_string lid
) r ctx
.lenv
in
251 ctx
<- { ctx
with lenv
= new_lenv }
252 | (Some Mut
, Mut
) -> ()
253 | (Some Readonly
, Readonly
) -> ()
256 (* List expressions require all of their lvals assigned to the readonlyness of the rval *)
257 List.iter el ~f
:(fun list_lval
-> self#assign env list_lval rval
)
258 | (_
, Class_get _
) -> () (* TODO: Static property access *)
259 (* TODO: make this exhaustive *)
262 (* Method call invocation *)
263 method method_call caller
=
264 let open Typing_defs
in
266 (* Method call checks *)
267 | ((_
, ty
), Obj_get
(e1
, _
, _
, (* is_prop_call *) false)) ->
268 let receiver_rty = self#ty_expr e1
in
269 (match (receiver_rty, get_node ty
) with
270 | (Readonly
, Tfun fty
) when not
(get_ft_readonly_this fty
) ->
271 Errors.readonly_method_call
(Tast.get_position e1
) (get_pos ty
)
275 (* Checks related to calling a function or method
276 is_readonly is true when the call is allowed to return readonly
282 (caller_ty
: Tast.ty
)
283 (args
: Tast.expr list
)
284 (unpacked_arg
: Tast.expr
option) =
285 let open Typing_defs
in
286 (* Check that function calls which return readonly are wrapped in readonly *)
287 let check_readonly_call caller_ty is_readonly
=
288 match get_node caller_ty
with
289 | Tfun fty
when get_ft_returns_readonly fty
->
290 if not is_readonly
then
291 Errors.explicit_readonly_cast
294 (Typing_defs.get_pos caller_ty
)
297 (* Checks a single arg against a parameter *)
298 let check_arg param arg
=
299 let param_rty = param_to_rty param
in
300 let arg_rty = self#ty_expr arg
in
301 if not
(subtype_rty arg_rty param_rty) then
302 Errors.readonly_mismatch
304 (Tast.get_position arg
)
307 ( Tast.get_position arg
,
308 "This expression is " ^
rty_to_str arg_rty );
313 "It is incompatible with this parameter, which is "
314 ^
rty_to_str param_rty );
316 (* Check fty const matching for an arg *)
318 (get_node param
.fp_type
.et_type
, get_node
(Tast.get_type arg
))
321 (* Passing a nonconst function to a const parameter *)
322 when get_fp_const_function param
323 && not
(Typing_defs.get_ft_is_const fty
) ->
324 Errors.readonly_mismatch
326 (Tast.get_position arg
)
329 ( Tast.get_position arg
,
330 "This function is not marked <<__ConstFun>>" );
335 "It is incompatible with this parameter, which is marked <<__ConstFun>>"
341 (* Check that readonly arguments match their parameters *)
342 let check_args caller_ty args unpacked_arg
=
343 match get_node caller_ty
with
345 let unpacked_rty = Option.to_list unpacked_arg
in
346 let args = args @ unpacked_rty in
347 (* If the args are unequal length, we errored elsewhere so this does not care *)
348 let _ = List.iter2 fty
.ft_params
args ~f
:check_arg in
352 check_readonly_call caller_ty is_readonly
;
353 check_args caller_ty
args unpacked_arg
355 method get_prop_elt env obj get
=
356 let open Typing_defs
in
357 match (get_node
(Tast.get_type obj
), get
) with
358 (* Basic case of a single class and a statically known id:
359 $x->prop (where $x : Foo) *)
360 | (Tclass
(id
, _exact
, _args
), (_, Id prop_id
)) ->
361 let provider_ctx = Tast_env.get_ctx env
in
362 (match Decl_provider.get_class
provider_ctx (snd id
) with
364 let prop = Cls.get_prop class_decl
(snd prop_id
) in
366 (* Class doesn't exist, assume mutable *)
368 (* TODO: Handle more complex generic cases *)
371 method obj_get env obj get
=
372 let prop_elt = self#get_prop_elt env obj get
in
373 match (prop_elt, self#ty_expr obj
) with
374 | (Some elt
, Mut
) when Typing_defs.get_ce_readonly_prop elt
->
375 Errors.explicit_readonly_cast
377 (Tast.get_position get
)
378 (Lazy.force elt
.Typing_defs.ce_pos
)
381 (* TODO: support obj get on generics, aliases and expression dependent types *)
382 method! on_method_ env m
=
383 let method_pos = fst m
.m_name
in
384 let ret_pos = Typing_defs.get_pos
(fst m
.m_ret
) in
386 if m
.m_readonly_this
then
387 Some
(Readonly
, method_pos)
389 Some
(Mut
, method_pos)
394 ret_ty
= Some
(readonly_kind_to_rty m
.m_readonly_ret
, ret_pos);
395 lenv
= lenv_from_params m
.m_params m
.m_user_attributes
;
399 super#on_method_ env m
401 method! on_fun_def env f
=
402 let ret_pos = Typing_defs.get_pos
(fst f
.f_ret
) in
403 let ret_ty = Some
(readonly_kind_to_rty f
.f_readonly_ret
, ret_pos) in
408 lenv
= lenv_from_params f
.f_params f
.f_user_attributes
;
412 super#on_fun_def env f
414 (* Normal functions go through on_fun_def, but all functions including closures go through on_fun_*)
415 method! on_fun_ env f
=
416 (* Copy the old ctx *)
417 let ret_pos = Typing_defs.get_pos
(fst f
.f_ret
) in
418 match ctx
.ret_ty with
419 (* If the ret pos is the same between both functions,
420 then this is just a fun_def, so ctx is correct already. Don't need to do anything *)
421 | Some
(_, outer_ret
) when Pos.equal outer_ret
ret_pos ->
424 (* Keep the old context for use later *)
426 (* First get the lenv from parameters, which override captured values *)
427 let is_const = has_const_attribute f
.f_user_attributes
in
428 (* If the lambda is const, we need to treat the entire lenv as if it is readonly, and all parameters as readonly *)
431 SMap.map
(fun _ -> Readonly
) ctx
.lenv
435 let new_lenv = lenv_from_params f
.f_params f
.f_user_attributes
in
436 let new_lenv = SMap.union
new_lenv old_lenv in
440 ret_ty = Some
(readonly_kind_to_rty f
.f_readonly_ret
, ret_pos);
445 let result = super#on_fun_ env f
in
446 (* Set the old context back *)
450 method! on_Foreach env e as_e b
=
451 (* foreach ($vec as $x)
452 The as expression always has the same readonlyness
453 as the collection in question. If it is readonly,
454 then the as expression's lvals are each assigned to readonly.
458 | Await_as_v
(_, lval
) ->
459 self#assign env lval e
461 | Await_as_kv
(_, l1
, l2
) ->
462 self#assign env l1 e
;
463 self#assign env l2 e
);
464 super#on_Foreach env e as_e b
466 method! on_expr env e
=
468 (* Property assignment *)
471 ( (Ast_defs.Eq
_ as bop
),
472 ((_, Obj_get
(obj
, get
, nullable
, is_prop_call
)) as lval
),
474 self#assign env lval rval
;
476 (* During a property assignment, skip the self#expr call to avoid erroring *)
477 self#on_Obj_get env obj get nullable is_prop_call
;
478 self#on_expr env rval
479 (* All other assignment *)
480 | (_, Binop
(Ast_defs.Eq
_, lval
, rval
)) ->
481 self#assign env lval rval
;
484 | (_, ReadonlyExpr
(_, Call
(caller
, targs
, args, unpacked_arg
))) ->
487 (Tast.get_position caller
)
488 (Tast.get_type caller
)
491 self#method_call caller
;
492 (* Skip the recursive step into ReadonlyExpr to avoid erroring *)
493 self#on_Call env caller targs
args unpacked_arg
494 (* Non readonly calls *)
495 | (_, Call
(caller
, _, args, unpacked_arg
)) ->
498 (Tast.get_position caller
)
499 (Tast.get_type caller
)
502 self#method_call caller
;
504 | (_, ReadonlyExpr
(_, Obj_get
(obj
, get
, nullable
, is_prop_call
))) ->
505 (* Skip the recursive step into ReadonlyExpr to avoid erroring *)
506 self#on_Obj_get env obj get nullable is_prop_call
507 | (_, Obj_get
(obj
, get
, _nullable
, _is_prop_call
)) ->
508 self#obj_get env obj get
;
510 | (_, New
(_, _, args, unpacked_arg
, (pos
, constructor_fty
))) ->
511 (* Constructors never return readonly, so that specific check is irrelevant *)
512 self#call ~is_readonly
:false pos constructor_fty
args unpacked_arg
513 | _ -> super#on_expr env e
515 method! on_stmt_ env s
=
518 (match ctx
.ret_ty with
519 | Some
(ret_ty, pos
) when not
(subtype_rty (self#ty_expr e
) ret_ty) ->
520 Errors.readonly_mismatch
522 (Tast.get_position e
)
523 ~reason_sub
:[(Tast.get_position e
, "This expression is readonly")]
524 ~reason_super
:[(pos
, "But this function does not return readonly.")]
525 (* If we don't have a ret ty we're not in a function, must have errored somewhere else *)
533 inherit Tast_visitor.handler_base
535 method! at_method_ env m
=
536 let tcopt = Tast_env.get_tcopt env
in
537 if TypecheckerOptions.readonly
tcopt then
538 check#on_method_ env m
542 method! at_fun_def env f
=
543 let tcopt = Tast_env.get_tcopt env
in
544 if TypecheckerOptions.readonly
tcopt then
545 check#on_fun_def env f