Fix bug with nullable member selection not erroring
[hiphop-php.git] / hphp / hack / src / typing / tast_check / readonly_check.ml
blob2307be226dcffa99fdb5b83261eb89d063b099a0
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
14 module MakeType = Typing_make_type
15 module Reason = Typing_reason
17 type rty =
18 | Readonly
19 | Mut [@deriving show]
21 let readonly_kind_to_rty = function
22 | Some Ast_defs.Readonly -> Readonly
23 | _ -> Mut
25 let rty_to_str = function
26 | Readonly -> "readonly"
27 | Mut -> "mutable"
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
38 | _ -> true
40 let param_to_rty param =
41 if Typing_defs.get_fp_readonly param then
42 Readonly
43 else
44 Mut
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 =
51 List.find_map
52 ~f:(fun ty ->
53 match grab_class_elts_from_ty ~static ~seen env ty prop_id with
54 | [] -> None
55 | tyl -> Some tyl)
56 tyl
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
63 | Some class_decl ->
64 let prop =
65 if static then
66 Cls.get_sprop class_decl (snd prop_id)
67 else
68 Cls.get_prop class_decl (snd prop_id)
70 Option.to_list prop
71 | None -> [])
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. *)
82 | Tunion tyl ->
83 List.concat_map
84 ~f:(fun ty -> grab_class_elts_from_ty ~static ~seen env ty prop_id)
85 tyl
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
92 else
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
100 | Toption ty ->
101 (* If it's nullable, take the *)
102 grab_class_elts_from_ty ~static ~seen env ty prop_id
103 | _ -> []
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
108 match get with
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 *)
111 | CGexpr _ -> []
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
116 match get with
117 | (_, _, Id prop_id) -> grab_class_elts_from_ty ~static:false env ty prop_id
118 (* TODO: Handle more complex cases *)
119 | _ -> []
121 let rec ty_expr env ((_, _, expr_) : Tast.expr) : rty =
122 match expr_ with
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
128 | Mut ->
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
131 let readonly_prop =
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
140 Readonly
141 else
143 | Array_get (array, _) -> ty_expr env array
144 | _ -> Mut
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(...)) *)
151 let shape =
152 MakeType.shape
153 Reason.none
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 *)
167 | Tprim _ -> true
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
185 false
186 else
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
190 | _ ->
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 =
216 if is_readonly then
218 else
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
223 "function call"
225 (Typing_defs.get_pos caller_ty)
226 | _ -> ()
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
235 | (Some elt, Mut) ->
236 Errors.explicit_readonly_cast
237 "property"
238 (Tast.get_position get)
239 (Lazy.force elt.ce_pos)
240 | _ -> ()
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
250 "static property"
252 (Lazy.force elt.Typing_defs.ce_pos)
253 | _ -> ()
255 let is_method_caller (caller : Tast.expr) =
256 match caller with
257 | (_, _, ReadonlyExpr (_, _, Obj_get (_, _, _, Is_method)))
258 | (_, _, Obj_get (_, _, _, Is_method)) ->
259 true
260 | _ -> false
262 let is_special_builtin = function
263 (* none of these functions require readonly checks, and can take in readonly values safely *)
264 | "HH\\dict"
265 | "HH\\varray"
266 | "HH\\darray"
267 | "HH\\vec"
268 | "HH\\keyset"
269 | "hphp_array_idx" ->
270 true
271 | _ -> false
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 =
276 let mutable_prop =
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)
284 ~reason_sub:
286 ( Tast.get_position rval |> Pos_or_decl.of_raw_pos,
287 "This expression is readonly" );
289 ~reason_super:
291 ( Lazy.force elt.Typing_defs.ce_pos,
292 "But it's being assigned to a mutable property" );
294 | _ -> ()
296 match lval with
297 (* List assignment *)
298 | (_, _, List exprs) -> List.iter exprs ~f:(fun lval -> assign env lval rval)
299 | (_, _, Array_get (array, _)) ->
300 begin
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. *)
305 (match array with
306 | (_, _, Array_get _)
307 | (_, _, Obj_get _) ->
308 assign env array rval
309 | _ -> ())
310 | (Mut, Readonly) ->
311 Errors.readonly_mismatch
312 "Invalid collection modification"
313 (Tast.get_position lval)
314 ~reason_sub:
316 ( Tast.get_position rval |> Pos_or_decl.of_raw_pos,
317 "This expression is readonly" );
319 ~reason_super:
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)
325 | (Mut, Mut) -> ()
327 | (_, _, Class_get (id, expr, Is_prop)) ->
328 (match ty_expr env rval with
329 | Readonly ->
330 let prop_elts = get_static_prop_elts env id expr in
331 check_ro_prop_assignment prop_elts
332 | _ -> ())
333 | (_, _, Obj_get (obj, get, _, Is_prop)) ->
334 (* Here to check for nested property accesses that are accessing readonly values *)
335 begin
336 match ty_expr env obj with
337 | Readonly -> Errors.readonly_modified (Tast.get_position obj)
338 | Mut -> ()
339 end;
340 (match ty_expr env rval with
341 | Readonly ->
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
345 | _ -> ())
346 (* TODO: make this exhaustive *)
347 | _ -> ()
349 (* Method call invocation *)
350 let method_call caller =
351 let open Typing_defs in
352 match caller with
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)
358 | _ -> ())
359 | _ -> ()
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
368 else
370 | _ -> ()
372 (* Checks related to calling a function or method
373 is_readonly is true when the call is allowed to return readonly
375 let call
376 ~is_readonly
377 ~method_call
378 (env : Tast_env.t)
379 (pos : Pos.t)
380 (caller_ty : Tast.ty)
381 (caller_rty : rty)
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
393 let suggestion =
394 match reason with
395 (* If we got this function from a typehint, we suggest marking the function (readonly function) *)
396 | Typing_reason.Rhint _ ->
397 let new_flags =
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
408 | _ -> ()
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
416 "Invalid argument"
417 (Tast.get_position arg)
418 ~reason_sub:
420 ( Tast.get_position arg |> Pos_or_decl.of_raw_pos,
421 "This expression is " ^ rty_to_str arg_rty );
423 ~reason_super:
425 ( param.fp_pos,
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
434 | Tfun fty ->
435 let unpacked_rty =
436 unpacked_arg
437 |> Option.map ~f:(fun e -> (Ast_defs.Pnormal, e))
438 |> Option.to_list
440 let args = args @ unpacked_rty in
441 let rec check args params =
442 match (args, params) with
443 | (x1 :: args1, x2 :: params2) ->
444 check_arg x2 x1;
445 check args1 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 *)
448 | ([], _)
449 | (_, []) ->
452 check args fty.ft_params
453 | _ -> ()
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 =
460 match caller with
461 | (_, _, Id (_, name)) when is_special_builtin (Utils.strip_ns name) -> true
462 | _ -> false
464 let check =
465 object (self)
466 inherit Tast_visitor.iter as super
468 method! on_expr env e =
469 match e with
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))) ->
474 let default () =
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
479 default ()
480 else
481 call
482 ~is_readonly:true
483 ~method_call:(is_method_caller caller)
485 (Tast.get_position caller)
486 (Tast.get_type caller)
487 (ty_expr env caller)
488 args
489 unpacked_arg;
490 check_special_function env caller args;
491 method_call caller;
492 default ()
493 (* Non readonly calls *)
494 | (_, _, Call (caller, _, args, unpacked_arg)) ->
495 if caller_is_special_builtin caller then
496 super#on_expr env e
497 else
498 call
500 ~is_readonly:false
501 ~method_call:(is_method_caller caller)
502 (Tast.get_position caller)
503 (Tast.get_type caller)
504 (ty_expr env caller)
505 args
506 unpacked_arg;
507 check_special_function env caller args;
508 method_call caller;
509 super#on_expr env e
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;
519 super#on_expr env e
520 | (_, pos, Class_get (class_, get, Is_prop)) ->
521 check_static_readonly_property pos env class_ get Mut;
522 super#on_expr env e
523 | (_, pos, New (_, _, args, unpacked_arg, constructor_fty)) ->
524 (* Constructors never return readonly, so that specific check is irrelevant *)
525 call
526 ~is_readonly:false
527 ~method_call:false
530 constructor_fty
532 (List.map ~f:(fun e -> (Ast_defs.Pnormal, e)) args)
533 unpacked_arg
534 | (_, _, Obj_get _)
535 | (_, _, Class_get _)
536 | (_, _, This)
537 | (_, _, ValCollection (_, _, _))
538 | (_, _, KeyValCollection (_, _, _))
539 | (_, _, Lvar _)
540 | (_, _, Clone _)
541 | (_, _, Array_get (_, _))
542 | (_, _, Yield _)
543 | (_, _, Await _)
544 | (_, _, Tuple _)
545 | (_, _, List _)
546 | (_, _, Cast (_, _))
547 | (_, _, Unop (_, _))
548 | (_, _, Pipe (_, _, _))
549 | (_, _, Eif (_, _, _))
550 | (_, _, Is (_, _))
551 | (_, _, As (_, _, _))
552 | (_, _, Upcast (_, _))
553 | (_, _, Import (_, _))
554 | (_, _, Lplaceholder _)
555 | (_, _, Pair (_, _, _))
556 | (_, _, ReadonlyExpr _)
557 | (_, _, Binop _)
558 | (_, _, ExpressionTree _)
559 | (_, _, Xml _)
560 | (_, _, Efun _)
561 (* Neither this nor any of the *_id expressions call the function *)
562 | (_, _, Method_caller (_, _))
563 | (_, _, Smethod_id (_, _))
564 | (_, _, Fun_id _)
565 | (_, _, Method_id _)
566 | (_, _, FunctionPointer _)
567 | (_, _, Lfun _)
568 | (_, _, Record _)
569 | (_, _, Null)
570 | (_, _, True)
571 | (_, _, False)
572 | (_, _, Omitted)
573 | (_, _, Id _)
574 | (_, _, Shape _)
575 | (_, _, EnumClassLabel _)
576 | (_, _, ET_Splice _)
577 | (_, _, Darray _)
578 | (_, _, Varray _)
579 | (_, _, Int _)
580 | (_, _, Dollardollar _)
581 | (_, _, String _)
582 | (_, _, String2 _)
583 | (_, _, Collection (_, _, _))
584 | (_, _, Class_const _)
585 | (_, _, Float _)
586 | (_, _, PrefixedString _)
587 | (_, _, Hole _) ->
588 super#on_expr env e
591 let handler =
592 object
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
603 ) else (
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
613 ) else (
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
627 else
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 *)
635 let check =
636 if !fun_has_readonly then
637 fun _e ->
639 else
640 fun e ->
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 *)
645 assign env lval rval
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
653 | _ -> ()
655 check e