Make ty_expr exhaustive on expressions
[hiphop-php.git] / hphp / hack / src / typing / tast_check / readonly_check.ml
blobefb3fcfd7ad36b6bd62db34540f85531b3be77db
1 (*
2 * Copyright (c) 2018, Facebook, Inc.
3 * All rights reserved.
5 * This source code is licensed under the MIT license found in the
6 * LICENSE file in the "hack" directory of this source tree.
8 *)
9 open Hh_prelude
10 open Aast
11 module Env = Tast_env
12 module Cls = Decl_provider.Class
13 module SN = Naming_special_names
15 type rty =
16 | Readonly
17 | Mut [@deriving show]
19 type ctx = {
20 lenv: rty SMap.t;
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
31 | _ -> Mut
33 let rty_to_str = function
34 | Readonly -> "readonly"
35 | Mut -> "mutable"
37 let has_const_attribute user_attributes =
38 List.exists user_attributes ~f:(fun ua ->
39 String.equal
40 (snd ua.ua_name)
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
52 List.fold_left
53 params
54 ~f:(fun acc p ->
55 SMap.add
56 p.param_name
57 (readonly_kind_to_rty
58 ( if constfun then
59 Some Ast_defs.Readonly
60 else
61 p.param_readonly ))
62 acc)
63 ~init:result
65 let get_local lenv id =
66 match SMap.find_opt id lenv with
67 | Some r -> r
68 | None -> Mut
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
76 | _ -> true
78 let param_to_rty param =
79 if Typing_defs.get_fp_readonly param then
80 Readonly
81 else
82 Mut
84 let check =
85 object (self)
86 inherit Tast_visitor.iter as super
88 val mutable ctx : ctx = empty_ctx
90 method ty_expr (e : Tast.expr) : rty =
91 match snd e with
92 | ReadonlyExpr _ -> Readonly
93 | This ->
94 (match ctx.this_ty with
95 | Some (r, _) -> r
96 | None -> Mut)
97 | Lvar (_, lid) ->
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
111 | Smethod_id _
112 | Method_caller _
113 | Call _ ->
115 (* All calls return mut by default, unless they are wrapped in a readonly expression *)
116 | Yield _ ->
117 Mut (* TODO: yield is a statement, really, not an expression. *)
118 | List _ ->
119 Mut (* Only appears as an lvalue; relevant in assign but not here *)
120 | Cast _ -> Mut
121 | Unop _ ->
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 *)
124 | Binop _ -> Mut
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
131 | Readonly -> true
132 | _ -> false)
133 then
134 Readonly
135 else
137 | ValCollection (_, _, el) ->
139 List.exists el ~f:(fun e ->
140 match self#ty_expr e with
141 | Readonly -> true
142 | _ -> false)
143 then
144 Readonly
145 else
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
150 | (Readonly, _)
151 | (_, Readonly) ->
152 Readonly
153 | _ -> Mut)
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
159 | (Readonly, _)
160 | (_, Readonly) ->
161 Readonly
162 | _ -> Mut)
163 | New _ -> Mut (* All constructors are mutable by default *)
164 (* Things that don't appear in function bodies generally *)
165 | Import _
166 | Callconv _ ->
168 | Lplaceholder _ -> Mut
169 (* Cloning something should always result in a mutable version of it *)
170 | Clone _ -> Mut
171 (* These are all value types without restrictions on mutability *)
172 | ExpressionTree _
173 | Xml _
174 | Efun _
175 | Any
176 | Fun_id _
177 | Method_id _
178 | Lfun _
179 | Record _
180 | FunctionPointer _
181 | Null
182 | True
183 | False
184 | Omitted
185 | Id _
186 | Shape _
187 | EnumAtom _
188 | ET_Splice _
189 | Darray _
190 | Varray _
191 | Int _
192 | Dollardollar _
193 | String _
194 | String2 _
195 | Collection (_, _, _)
196 | Float _
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 =
203 match lval with
204 | (_, Array_get (array, _)) ->
205 (* TODO: appending to readonly value types is technically allowed *)
206 begin
207 match self#ty_expr array with
208 | Readonly -> Errors.readonly_modified (Tast.get_position array)
209 | Mut -> ()
211 | (_, Obj_get (obj, get, _, _)) ->
212 begin
213 match self#ty_expr obj with
214 | Readonly -> Errors.readonly_modified (Tast.get_position obj)
215 | Mut -> ()
216 end;
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)
224 ~reason_sub:
225 [(Tast.get_position rval, "This expression is readonly")]
226 ~reason_super:
228 ( Lazy.force elt.Typing_defs.ce_pos,
229 "But it's being assigned to a mutable property" );
231 | _ -> ())
232 | (_, Lvar (_, lid)) ->
233 let var_ro_opt = SMap.find_opt (Local_id.to_string lid) ctx.lenv in
234 begin
235 match (var_ro_opt, self#ty_expr rval) with
236 | (Some Readonly, Mut) ->
237 Errors.var_readonly_mismatch
238 (Tast.get_position lval)
239 "readonly"
240 (Tast.get_position rval)
241 "mutable"
242 | (Some Mut, Readonly) ->
243 Errors.var_readonly_mismatch
244 (Tast.get_position lval)
245 "mutable"
246 (Tast.get_position rval)
247 "readonly"
248 | (None, r) ->
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) -> ()
255 | (_, List el) ->
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 *)
260 | _ -> ()
262 (* Method call invocation *)
263 method method_call caller =
264 let open Typing_defs in
265 match caller with
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)
272 | _ -> ())
273 | _ -> ()
275 (* Checks related to calling a function or method
276 is_readonly is true when the call is allowed to return readonly
277 TODO: handle inout
279 method call
280 ~is_readonly
281 (pos : Pos.t)
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
292 "function call"
294 (Typing_defs.get_pos caller_ty)
295 | _ -> ()
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
303 "Invalid argument"
304 (Tast.get_position arg)
305 ~reason_sub:
307 ( Tast.get_position arg,
308 "This expression is " ^ rty_to_str arg_rty );
310 ~reason_super:
312 ( param.fp_pos,
313 "It is incompatible with this parameter, which is "
314 ^ rty_to_str param_rty );
316 (* Check fty const matching for an arg *)
317 match
318 (get_node param.fp_type.et_type, get_node (Tast.get_type arg))
319 with
320 | (Tfun _, Tfun fty)
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
325 "Invalid argument"
326 (Tast.get_position arg)
327 ~reason_sub:
329 ( Tast.get_position arg,
330 "This function is not marked <<__ConstFun>>" );
332 ~reason_super:
334 ( param.fp_pos,
335 "It is incompatible with this parameter, which is marked <<__ConstFun>>"
338 | _ -> ()
341 (* Check that readonly arguments match their parameters *)
342 let check_args caller_ty args unpacked_arg =
343 match get_node caller_ty with
344 | Tfun fty ->
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
350 | _ -> ()
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
363 | Some class_decl ->
364 let prop = Cls.get_prop class_decl (snd prop_id) in
365 prop
366 (* Class doesn't exist, assume mutable *)
367 | None -> None)
368 (* TODO: Handle more complex generic cases *)
369 | _ -> None
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
376 "property"
377 (Tast.get_position get)
378 (Lazy.force elt.Typing_defs.ce_pos)
379 | _ -> ()
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
385 let this_ty =
386 if m.m_readonly_this then
387 Some (Readonly, method_pos)
388 else
389 Some (Mut, method_pos)
391 let new_ctx =
393 this_ty;
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;
398 ctx <- new_ctx;
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
404 let new_ctx =
406 this_ty = None;
407 ret_ty;
408 lenv = lenv_from_params f.f_params f.f_user_attributes;
411 ctx <- new_ctx;
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 ->
422 super#on_fun_ env f
423 | _ ->
424 (* Keep the old context for use later *)
425 let old_ctx = ctx in
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 *)
429 let old_lenv =
430 if is_const then
431 SMap.map (fun _ -> Readonly) ctx.lenv
432 else
433 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
437 let new_ctx =
439 this_ty = None;
440 ret_ty = Some (readonly_kind_to_rty f.f_readonly_ret, ret_pos);
441 lenv = new_lenv;
444 ctx <- new_ctx;
445 let result = super#on_fun_ env f in
446 (* Set the old context back *)
447 ctx <- old_ctx;
448 result
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.
456 (match as_e with
457 | As_v lval
458 | Await_as_v (_, lval) ->
459 self#assign env lval e
460 | As_kv (l1, l2)
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 =
467 match e with
468 (* Property assignment *)
469 | ( _,
470 Binop
471 ( (Ast_defs.Eq _ as bop),
472 ((_, Obj_get (obj, get, nullable, is_prop_call)) as lval),
473 rval ) ) ->
474 self#assign env lval rval;
475 self#on_bop env bop;
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;
482 super#on_expr env e
483 (* Readonly calls *)
484 | (_, ReadonlyExpr (_, Call (caller, targs, args, unpacked_arg))) ->
485 self#call
486 ~is_readonly:true
487 (Tast.get_position caller)
488 (Tast.get_type caller)
489 args
490 unpacked_arg;
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)) ->
496 self#call
497 ~is_readonly:false
498 (Tast.get_position caller)
499 (Tast.get_type caller)
500 args
501 unpacked_arg;
502 self#method_call caller;
503 super#on_expr env e
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;
509 super#on_expr env e
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 =
516 (match s with
517 | Return (Some e) ->
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
521 "Invalid return"
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 *)
526 | _ -> ())
527 | _ -> ());
528 super#on_stmt_ env s
531 let handler =
532 object
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
539 else
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
546 else