source/interprocedural/callGraph.ml (1,619 lines of code) (raw):

(* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. *) open Core open Analysis open Ast open Statement open Expression open Pyre module CallTarget = struct type t = { target: Target.t; implicit_self: bool; implicit_dunder_call: bool; collapse_tito: bool; index: int; receiver_type: Type.t option; } [@@deriving compare, eq, show { with_path = false }] let target { target; _ } = target let equal_ignoring_indices left right = equal left { right with index = left.index } let dedup_and_sort targets = targets |> List.sort ~compare |> List.remove_consecutive_duplicates ~which_to_keep:`First ~equal:equal_ignoring_indices let create ?(implicit_self = false) ?(implicit_dunder_call = false) ?(collapse_tito = true) ?(index = 0) ?receiver_type target = { target; implicit_self; implicit_dunder_call; collapse_tito; index; receiver_type } let equal_ignoring_types { target = target_left; implicit_self = implicit_self_left; implicit_dunder_call = implicit_dunder_call_left; collapse_tito = collapse_tito_left; index = index_left; receiver_type = _; } { target = target_right; implicit_self = implicit_self_right; implicit_dunder_call = implicit_dunder_call_right; collapse_tito = collapse_tito_right; index = index_right; receiver_type = _; } = Target.equal target_left target_right && implicit_self_left == implicit_self_right && implicit_dunder_call_left == implicit_dunder_call_right && collapse_tito_left == collapse_tito_right && index_left == index_right end module HigherOrderParameter = struct type t = { index: int; call_targets: CallTarget.t list; return_type: Type.t; } [@@deriving eq, show { with_path = false }] let join left right = match left, right with | Some { index = left_index; _ }, Some { index = right_index; _ } -> if left_index <= right_index then left else right | Some _, None -> left | None, Some _ -> right | None, None -> None let all_targets { call_targets; _ } = List.map ~f:CallTarget.target call_targets let equal_ignoring_types { index = index_left; call_targets = call_targets_left; return_type = _ } { index = index_right; call_targets = call_targets_right; return_type = _ } = index_left == index_right && List.equal CallTarget.equal call_targets_left call_targets_right end module CallCallees = struct type t = { call_targets: CallTarget.t list; new_targets: CallTarget.t list; init_targets: CallTarget.t list; return_type: Type.t; higher_order_parameter: HigherOrderParameter.t option; unresolved: bool; } [@@deriving eq, show { with_path = false }] let create ?(call_targets = []) ?(new_targets = []) ?(init_targets = []) ?higher_order_parameter ?(unresolved = false) ~return_type () = { call_targets; new_targets; init_targets; return_type; higher_order_parameter; unresolved } let create_unresolved return_type = { call_targets = []; new_targets = []; init_targets = []; return_type; higher_order_parameter = None; unresolved = true; } let is_partially_resolved = function | { call_targets = _ :: _; _ } -> true | { new_targets = _ :: _; _ } -> true | { init_targets = _ :: _; _ } -> true | _ -> false let pp_option formatter = function | None -> Format.fprintf formatter "None" | Some callees -> pp formatter callees let join { call_targets = left_call_targets; new_targets = left_new_targets; init_targets = left_init_targets; return_type; higher_order_parameter = left_higher_order_parameter; unresolved = left_unresolved; } { call_targets = right_call_targets; new_targets = right_new_targets; init_targets = right_init_targets; return_type = _; higher_order_parameter = right_higher_order_parameter; unresolved = right_unresolved; } = let call_targets = List.rev_append left_call_targets right_call_targets in let new_targets = List.rev_append left_new_targets right_new_targets in let init_targets = List.rev_append left_init_targets right_init_targets in let higher_order_parameter = HigherOrderParameter.join left_higher_order_parameter right_higher_order_parameter in let unresolved = left_unresolved || right_unresolved in { call_targets; new_targets; init_targets; return_type; higher_order_parameter; unresolved } let deduplicate { call_targets; new_targets; init_targets; return_type; higher_order_parameter; unresolved } = let call_targets = CallTarget.dedup_and_sort call_targets in let new_targets = CallTarget.dedup_and_sort new_targets in let init_targets = CallTarget.dedup_and_sort init_targets in let higher_order_parameter = match higher_order_parameter with | Some { HigherOrderParameter.index; call_targets; return_type } -> Some { HigherOrderParameter.index; call_targets = CallTarget.dedup_and_sort call_targets; return_type; } | None -> None in { call_targets; new_targets; init_targets; return_type; higher_order_parameter; unresolved } let all_targets { call_targets; new_targets; init_targets; higher_order_parameter; _ } = call_targets |> List.rev_append new_targets |> List.rev_append init_targets |> List.map ~f:CallTarget.target |> List.rev_append (higher_order_parameter >>| HigherOrderParameter.all_targets |> Option.value ~default:[]) let equal_ignoring_types { call_targets = call_targets_left; new_targets = new_targets_left; init_targets = init_targets_left; return_type = _; higher_order_parameter = higher_order_parameter_left; unresolved = unresolved_left; } { call_targets = call_targets_right; new_targets = new_targets_right; init_targets = init_targets_right; return_type = _; higher_order_parameter = higher_order_parameter_right; unresolved = unresolved_right; } = List.equal CallTarget.equal_ignoring_types call_targets_left call_targets_right && List.equal CallTarget.equal_ignoring_types new_targets_left new_targets_right && List.equal CallTarget.equal_ignoring_types init_targets_left init_targets_right && Option.equal HigherOrderParameter.equal_ignoring_types higher_order_parameter_left higher_order_parameter_right && unresolved_left == unresolved_right end module AttributeAccessCallees = struct type t = { property_targets: CallTarget.t list; global_targets: CallTarget.t list; return_type: Type.t; is_attribute: bool; } [@@deriving eq, show { with_path = false }] let deduplicate { property_targets; global_targets; return_type; is_attribute } = { property_targets = CallTarget.dedup_and_sort property_targets; global_targets = CallTarget.dedup_and_sort global_targets; return_type; is_attribute; } let join { property_targets = left_property_targets; global_targets = left_global_targets; return_type; is_attribute = left_is_attribute; } { property_targets = right_property_targets; global_targets = right_global_targets; return_type = _; is_attribute = right_is_attribute; } = { property_targets = List.rev_append left_property_targets right_property_targets; global_targets = List.rev_append left_global_targets right_global_targets; return_type; is_attribute = left_is_attribute || right_is_attribute; } let all_targets { property_targets; global_targets; _ } = List.rev_append property_targets global_targets |> List.map ~f:CallTarget.target let equal_ignoring_types { property_targets = property_targets_left; global_targets = global_targets_left; is_attribute = is_attribute_left; return_type = _; } { property_targets = property_targets_right; global_targets = global_targets_right; is_attribute = is_attribute_right; return_type = _; } = List.equal CallTarget.equal property_targets_left property_targets_right && List.equal CallTarget.equal global_targets_left global_targets_right && is_attribute_left == is_attribute_right let empty = { property_targets = []; global_targets = []; is_attribute = true; return_type = Type.none } let is_empty attribute_access_callees = equal_ignoring_types attribute_access_callees empty end module IdentifierCallees = struct type t = { global_targets: CallTarget.t list } [@@deriving eq, show { with_path = false }] let deduplicate { global_targets } = { global_targets = CallTarget.dedup_and_sort global_targets } let join { global_targets = left_global_targets } { global_targets = right_global_targets } = { global_targets = List.rev_append left_global_targets right_global_targets } let all_targets { global_targets } = List.map ~f:CallTarget.target global_targets end module FormatStringCallees = struct type t = { call_targets: CallTarget.t list } [@@deriving eq, show { with_path = false }] let deduplicate { call_targets } = { call_targets = List.dedup_and_sort ~compare:CallTarget.compare call_targets } let join { call_targets = left_call_targets } { call_targets = right_call_targets } = { call_targets = List.rev_append left_call_targets right_call_targets } let all_targets { call_targets } = List.map ~f:CallTarget.target call_targets end module ExpressionCallees = struct type t = { call: CallCallees.t option; attribute_access: AttributeAccessCallees.t option; identifier: IdentifierCallees.t option; format_string: FormatStringCallees.t option; } [@@deriving eq, show { with_path = false }] let from_call callees = { call = Some callees; attribute_access = None; identifier = None; format_string = None } let from_call_with_empty_attribute callees = { call = Some callees; attribute_access = Some AttributeAccessCallees.empty; identifier = None; format_string = None; } let from_attribute_access properties = { call = None; attribute_access = Some properties; identifier = None; format_string = None } let from_identifier identifier = { call = None; attribute_access = None; identifier = Some identifier; format_string = None } let from_format_string format_string = { call = None; attribute_access = None; identifier = None; format_string = Some format_string } let join { call = left_call; attribute_access = left_attribute_access; identifier = left_identifier; format_string = left_format_string; } { call = right_call; attribute_access = right_attribute_access; identifier = right_identifier; format_string = right_format_string; } = { call = Option.merge ~f:CallCallees.join left_call right_call; attribute_access = Option.merge ~f:AttributeAccessCallees.join left_attribute_access right_attribute_access; identifier = Option.merge ~f:IdentifierCallees.join left_identifier right_identifier; format_string = Option.merge ~f:FormatStringCallees.join left_format_string right_format_string; } let deduplicate { call; attribute_access; identifier; format_string } = { call = call >>| CallCallees.deduplicate; attribute_access = attribute_access >>| AttributeAccessCallees.deduplicate; identifier = identifier >>| IdentifierCallees.deduplicate; format_string = format_string >>| FormatStringCallees.deduplicate; } let all_targets { call; attribute_access; identifier; format_string } = let call_targets = call >>| CallCallees.all_targets |> Option.value ~default:[] in let attribute_access_targets = attribute_access >>| AttributeAccessCallees.all_targets |> Option.value ~default:[] in let identifier_targets = identifier >>| IdentifierCallees.all_targets |> Option.value ~default:[] in let format_string_targets = format_string >>| FormatStringCallees.all_targets |> Option.value ~default:[] in call_targets |> List.rev_append attribute_access_targets |> List.rev_append identifier_targets |> List.rev_append format_string_targets let is_empty_attribute_access_callees = function | { call = None; attribute_access = Some some_attribute_access; identifier = None; format_string = None; } -> AttributeAccessCallees.is_empty some_attribute_access | _ -> false let equal_ignoring_types { call = call_left; attribute_access = attribute_access_left; identifier = identifier_left; format_string = format_string_left; } { call = call_right; attribute_access = attribute_access_right; identifier = identifier_right; format_string = format_string_right; } = Option.equal CallCallees.equal_ignoring_types call_left call_right && Option.equal AttributeAccessCallees.equal_ignoring_types attribute_access_left attribute_access_right && Option.equal IdentifierCallees.equal identifier_left identifier_right && Option.equal FormatStringCallees.equal format_string_left format_string_right end module LocationCallees = struct type t = | Singleton of ExpressionCallees.t | Compound of ExpressionCallees.t String.Map.Tree.t [@@deriving eq] let pp formatter = function | Singleton callees -> Format.fprintf formatter "%a" ExpressionCallees.pp callees | Compound map -> String.Map.Tree.to_alist map |> List.map ~f:(fun (key, value) -> Format.asprintf "%s: %a" key ExpressionCallees.pp value) |> String.concat ~sep:", " |> Format.fprintf formatter "%s" let show callees = Format.asprintf "%a" pp callees let all_targets = function | Singleton raw_callees -> ExpressionCallees.all_targets raw_callees | Compound map -> String.Map.Tree.data map |> List.concat_map ~f:ExpressionCallees.all_targets let equal_ignoring_types location_callees_left location_callees_right = match location_callees_left, location_callees_right with | Singleton callees_left, Singleton callees_right -> ExpressionCallees.equal_ignoring_types callees_left callees_right | Compound map_left, Compound map_right -> String.Map.Tree.equal ExpressionCallees.equal_ignoring_types map_left map_right | _ -> false end module UnprocessedLocationCallees = struct type t = ExpressionCallees.t String.Map.Tree.t let singleton ~expression_identifier ~callees = String.Map.Tree.singleton expression_identifier callees let add map ~expression_identifier ~callees = String.Map.Tree.update map expression_identifier ~f:(function | Some existing_callees -> ExpressionCallees.join existing_callees callees | None -> callees) end let call_identifier { Call.callee; _ } = match Node.value callee with | Name (Name.Attribute { attribute; _ }) -> attribute | Name (Name.Identifier name) -> name | _ -> (* Fall back to something that hopefully identifies the call well. *) Expression.show callee let expression_identifier = function | Expression.Call call -> Some (call_identifier call) | Expression.Name (Name.Attribute { attribute; _ }) -> Some attribute | _ -> (* not a valid call site. *) None module DefineCallGraph = struct type t = LocationCallees.t Location.Map.t [@@deriving eq] let pp formatter call_graph = let pp_pair formatter (key, value) = Format.fprintf formatter "@,%a -> %a" Location.pp key LocationCallees.pp value in let pp_pairs formatter = List.iter ~f:(pp_pair formatter) in call_graph |> Location.Map.to_alist |> Format.fprintf formatter "{@[<v 2>%a@]@,}" pp_pairs let show = Format.asprintf "%a" pp let empty = Location.Map.empty let add call_graph ~location ~callees = Location.Map.set call_graph ~key:location ~data:callees let resolve_expression call_graph ~location ~expression_identifier = match Location.Map.find call_graph location with | Some (LocationCallees.Singleton callees) -> Some callees | Some (LocationCallees.Compound name_to_callees) -> String.Map.Tree.find name_to_callees expression_identifier | None -> None let resolve_call call_graph ~location ~call = expression_identifier (Expression.Call call) >>= fun expression_identifier -> resolve_expression call_graph ~location ~expression_identifier >>= fun { call; _ } -> call let resolve_attribute_access call_graph ~location ~attribute = resolve_expression call_graph ~location ~expression_identifier:attribute >>= fun { attribute_access; _ } -> attribute_access let resolve_identifier call_graph ~location ~identifier = resolve_expression call_graph ~location ~expression_identifier:identifier >>= fun { identifier; _ } -> identifier let format_string_expression_identifier = "$__str__$" let resolve_format_string call_graph ~location = resolve_expression call_graph ~location ~expression_identifier:format_string_expression_identifier >>= fun { format_string; _ } -> format_string let equal_ignoring_types call_graph_left call_graph_right = Location.Map.equal LocationCallees.equal_ignoring_types call_graph_left call_graph_right end (* Produce call targets with a textual order index. * * The index is the number of times a given function or method was previously called, * respecting the execution flow. * * ``` * def f(): * a = source_with_hop() # index=0 * sink_with_hop(x=a) # index=0 * sink_with_hop(y=a) # index=1 * b = source_with_hop() # index=1 * sink_with_hop(z=a) # index=2 * ``` *) module CallTargetIndexer = struct type t = { indices: int Target.HashMap.t; mutable seen_targets: Target.Set.t; } let create () = { indices = Target.HashMap.create (); seen_targets = Target.Set.empty } let generate_fresh_indices indexer = Target.Set.iter (Target.HashMap.incr indexer.indices) indexer.seen_targets; indexer.seen_targets <- Target.Set.empty let create_target indexer ~implicit_self ~implicit_dunder_call ~collapse_tito ?receiver_type original_target = let target_for_index = Target.override_to_method original_target in let index = Target.HashMap.find indexer.indices target_for_index |> Option.value ~default:0 in indexer.seen_targets <- Target.Set.add target_for_index indexer.seen_targets; { CallTarget.target = original_target; implicit_self; implicit_dunder_call; collapse_tito; index; receiver_type; } end let defining_attribute ~resolution parent_type attribute = let global_resolution = Resolution.global_resolution resolution in Type.split parent_type |> fst |> Type.primitive_name >>= fun class_name -> GlobalResolution.attribute_from_class_name ~transitive:true ~resolution:global_resolution ~name:attribute ~instantiated:parent_type class_name >>= fun instantiated_attribute -> if Annotated.Attribute.defined instantiated_attribute then Some instantiated_attribute else Resolution.fallback_attribute ~resolution ~name:attribute class_name let rec resolve_ignoring_optional ~resolution expression = let resolve_expression_to_type expression = match Resolution.resolve_expression_to_type resolution expression, Node.value expression with | ( Type.Callable ({ Type.Callable.kind = Anonymous; _ } as callable), Expression.Name (Name.Identifier function_name) ) when function_name |> String.is_prefix ~prefix:"$local_" -> (* Treat nested functions as named callables. *) Type.Callable { callable with kind = Named (Reference.create function_name) } | annotation, _ -> annotation in let annotation = match Node.value expression with | Expression.Name (Name.Attribute { base; attribute; _ }) -> ( let base_type = resolve_ignoring_optional ~resolution base |> fun annotation -> Type.optional_value annotation |> Option.value ~default:annotation in match defining_attribute ~resolution base_type attribute with | Some _ -> Resolution.resolve_attribute_access resolution ~base_type ~attribute | None -> resolve_expression_to_type expression (* Lookup the base_type for the attribute you were interested in *)) | _ -> resolve_expression_to_type expression in Type.optional_value annotation |> Option.value ~default:annotation type callee_kind = | Method of { is_direct_call: bool } | Function let is_local identifier = String.is_prefix ~prefix:"$" identifier let rec is_all_names = function | Expression.Name (Name.Identifier identifier) when not (is_local identifier) -> true | Name (Name.Attribute { base; attribute; _ }) when not (is_local attribute) -> is_all_names (Node.value base) | _ -> false let rec callee_kind ~resolution callee callee_type = let is_super_call = let rec is_super callee = match Node.value callee with | Expression.Call { callee = { Node.value = Name (Name.Identifier "super"); _ }; _ } -> true | Call { callee; _ } -> is_super callee | Name (Name.Attribute { base; _ }) -> is_super base | _ -> false in is_super callee in match callee_type with | _ when is_super_call -> Method { is_direct_call = true } | Type.Parametric { name = "BoundMethod"; _ } -> Method { is_direct_call = is_all_names (Node.value callee) } | Type.Callable _ -> ( match Node.value callee with | Expression.Name (Name.Attribute { base; _ }) -> let parent_type = resolve_ignoring_optional ~resolution base in let is_class () = parent_type |> GlobalResolution.class_definition (Resolution.global_resolution resolution) |> Option.is_some in if Type.is_meta parent_type then Method { is_direct_call = true } else if is_class () then Method { is_direct_call = false } else Function | _ -> Function) | Type.Union (callee_type :: _) -> callee_kind ~resolution callee callee_type | _ -> (* We must be dealing with a callable class. *) Method { is_direct_call = false } let strip_optional annotation = Type.optional_value annotation |> Option.value ~default:annotation let strip_meta annotation = if Type.is_meta annotation then Type.single_parameter annotation else annotation (* Figure out what target to pick for an indirect call that resolves to implementation_target. E.g., if the receiver type is A, and A derives from Base, and the target is Base.method, then targeting the override tree of Base.method is wrong, as it would include all siblings for A. * Instead, we have the following cases: * a) receiver type matches implementation_target's declaring type -> override implementation_target * b) no implementation_target override entries are subclasses of A -> real implementation_target * c) some override entries are subclasses of A -> search upwards for actual implementation, * and override all those where the override name is * 1) the override target if it exists in the override shared mem * 2) the real target otherwise *) let compute_indirect_targets ~resolution ~receiver_type implementation_target = (* Target name must be the resolved implementation target *) let global_resolution = Resolution.global_resolution resolution in let get_class_type = GlobalResolution.parse_reference global_resolution in let get_actual_target method_name = if DependencyGraphSharedMemory.overrides_exist method_name then Target.create_override method_name else Target.create_method method_name in let receiver_type = receiver_type |> strip_meta |> strip_optional |> Type.weaken_literals in let declaring_type = Reference.prefix implementation_target in if declaring_type >>| Reference.equal (Type.class_name receiver_type) |> Option.value ~default:false then (* case a *) [get_actual_target implementation_target] else let target_callable = Target.create_method implementation_target in match DependencyGraphSharedMemory.get_overriding_types ~member:implementation_target with | None -> (* case b *) [target_callable] | Some overriding_types -> (* case c *) let keep_subtypes candidate = let candidate_type = get_class_type candidate in GlobalResolution.less_or_equal global_resolution ~left:candidate_type ~right:receiver_type in let override_targets = let create_override_target class_name = let method_name = Reference.last implementation_target in Reference.create ~prefix:class_name method_name |> get_actual_target in List.filter overriding_types ~f:keep_subtypes |> fun subtypes -> List.map subtypes ~f:create_override_target in target_callable :: override_targets let collapse_tito ~resolution ~callee ~callable_type = (* For most cases, it is simply incorrect to not collapse tito, as it will lead to incorrect * mapping from input to output taint. However, the collapsing of tito adversely affects our * analysis in the case of the builder pattern, i.e. * * class C: * def set_field(self, field) -> "C": * self.field = field * return self * * In this case, collapsing tito leads to field * tainting the entire `self` for chained call. To prevent this problem, we special case * builders to preserve the tito structure. *) match callable_type with | Type.Parametric { name = "BoundMethod"; parameters = [_; Type.Parameter.Single implicit] } -> let return_annotation = (* To properly substitute type variables, we simulate `callee.__call__` for the bound method. *) let to_simulate = Node.create_with_default_location (Expression.Name (Name.Attribute { base = callee; attribute = "__call__"; special = true })) in Resolution.resolve_expression resolution to_simulate |> snd |> function | Type.Callable { Type.Callable.implementation; _ } -> Type.Callable.Overload.return_annotation implementation | _ -> Type.Top in not (Type.equal implicit return_annotation) | _ -> true let rec resolve_callees_from_type ~resolution ~call_indexer ?(dunder_call = false) ?receiver_type ~return_type ~callee_kind ~collapse_tito callable_type = let resolve_callees_from_type ?(dunder_call = dunder_call) = resolve_callees_from_type ~dunder_call in match callable_type with | Type.Callable { kind = Named name; _ } -> ( match receiver_type with | Some receiver_type -> let targets = match callee_kind with | Method { is_direct_call = true } -> [Target.create_method name] | _ -> compute_indirect_targets ~resolution ~receiver_type name in let targets = List.map ~f:(fun target -> CallTargetIndexer.create_target call_indexer ~implicit_self:true ~implicit_dunder_call:dunder_call ~collapse_tito ~receiver_type target) targets in CallCallees.create ~call_targets:targets ~return_type () | None -> let target = match callee_kind with | Method _ -> Target.create_method name | _ -> Target.create_function name in CallCallees.create ~call_targets: [ CallTargetIndexer.create_target call_indexer ~implicit_self:false ~implicit_dunder_call:dunder_call ~collapse_tito ?receiver_type target; ] ~return_type ()) | Type.Callable { kind = Anonymous; _ } -> CallCallees.create_unresolved return_type | Type.Parametric { name = "BoundMethod"; parameters = [Single callable; Single receiver_type] } -> resolve_callees_from_type ~resolution ~call_indexer ~receiver_type ~return_type ~callee_kind ~collapse_tito callable | Type.Union (element :: elements) -> let first_targets = resolve_callees_from_type ~resolution ~call_indexer ~callee_kind ?receiver_type ~return_type ~collapse_tito element in List.fold elements ~init:first_targets ~f:(fun combined_targets new_target -> resolve_callees_from_type ~resolution ~call_indexer ?receiver_type ~return_type ~callee_kind ~collapse_tito new_target |> CallCallees.join combined_targets) | Type.Parametric { name = "type"; _ } -> resolve_constructor_callee ~resolution ~call_indexer ~return_type callable_type |> Option.value ~default:(CallCallees.create_unresolved return_type) | callable_type -> ( (* Handle callable classes. `typing.Type` interacts specially with __call__, so we choose to ignore it for now to make sure our constructor logic via `cls()` still works. *) match Resolution.resolve_attribute_access resolution ~base_type:callable_type ~attribute:"__call__" with | Type.Any | Type.Top -> CallCallees.create_unresolved return_type (* Callable protocol. *) | Type.Callable { kind = Anonymous; _ } -> Type.primitive_name callable_type >>| (fun primitive_callable_name -> let target = `Method { Target.class_name = primitive_callable_name; method_name = "__call__" } in CallCallees.create ~call_targets: [ CallTargetIndexer.create_target call_indexer ~implicit_self:true ~implicit_dunder_call:true ~collapse_tito ?receiver_type target; ] ~return_type ()) |> Option.value ~default:(CallCallees.create_unresolved return_type) | annotation -> if not dunder_call then resolve_callees_from_type ~resolution ~call_indexer ~return_type ~dunder_call:true ~callee_kind ~collapse_tito annotation else CallCallees.create_unresolved return_type) and resolve_constructor_callee ~resolution ~call_indexer ~return_type class_type = match ( Resolution.resolve_attribute_access resolution ~base_type:class_type ~attribute:"__new__", Resolution.resolve_attribute_access resolution ~base_type:class_type ~attribute:"__init__" ) with | Type.Any, _ | Type.Top, _ | _, Type.Any | _, Type.Top -> None | new_callable_type, init_callable_type -> let new_callees = resolve_callees_from_type ~resolution ~call_indexer ~receiver_type:class_type ~return_type ~callee_kind:(Method { is_direct_call = true }) ~collapse_tito:true new_callable_type in let init_callees = resolve_callees_from_type ~resolution ~call_indexer ~receiver_type:class_type ~return_type ~callee_kind:(Method { is_direct_call = true }) ~collapse_tito:true init_callable_type in Some (CallCallees.create ~new_targets:new_callees.call_targets ~init_targets:init_callees.call_targets ~unresolved:(new_callees.unresolved || init_callees.unresolved) ~return_type ()) let resolve_callee_from_defining_expression ~resolution ~call_indexer ~callee:{ Node.value = callee; _ } ~return_type ~implementing_class = match implementing_class, callee with | Type.Top, Expression.Name name when is_all_names callee -> (* If implementing_class is unknown, this must be a function rather than a method. We can use global resolution on the callee. *) GlobalResolution.global (Resolution.global_resolution resolution) (Ast.Expression.name_to_reference_exn name) >>= fun { AttributeResolution.Global.undecorated_signature; _ } -> undecorated_signature >>| fun undecorated_signature -> resolve_callees_from_type ~resolution ~call_indexer ~return_type ~callee_kind:Function ~collapse_tito:true (Type.Callable undecorated_signature) | _ -> ( let implementing_class_name = if Type.is_meta implementing_class then Type.parameters implementing_class >>= fun parameters -> List.nth parameters 0 >>= function | Single implementing_class -> Some implementing_class | _ -> None else Some implementing_class in match implementing_class_name with | Some implementing_class_name -> let class_primitive = match implementing_class_name with | Parametric { name; _ } -> Some name | Primitive name -> Some name | _ -> None in let method_name = match callee with | Expression.Name (Name.Attribute { attribute; _ }) -> Some attribute | _ -> None in method_name >>= (fun method_name -> class_primitive >>| fun class_name -> Format.sprintf "%s.%s" class_name method_name) >>| Reference.create (* Here, we blindly reconstruct the callable instead of going through the global resolution, as Pyre doesn't have an API to get the undecorated signature of methods. *) >>= fun name -> let callable_type = Type.Callable { Type.Callable.kind = Named name; implementation = { annotation = Type.Any; parameters = Type.Callable.Defined [] }; overloads = []; } in Some (resolve_callees_from_type ~resolution ~call_indexer ~receiver_type:implementing_class ~return_type ~callee_kind:(Method { is_direct_call = false }) ~collapse_tito:true callable_type) | _ -> None) let transform_special_calls ~resolution { Call.callee; arguments } = match callee, arguments with | ( { Node.value = Expression.Name (Name.Attribute { base = { Node.value = Expression.Name (Name.Identifier "functools"); _ }; attribute = "partial"; _; }); _; }, { Call.Argument.value = actual_callable; _ } :: actual_arguments ) -> Some { Call.callee = actual_callable; arguments = actual_arguments } | ( { Node.value = Name (Name.Attribute { base = { Node.value = Expression.Name (Name.Identifier "multiprocessing"); _ }; attribute = "Process"; _; }); _; }, [ { Call.Argument.value = process_callee; name = Some { Node.value = "$parameter$target"; _ } }; { Call.Argument.value = { Node.value = Expression.Tuple process_arguments; _ }; name = Some { Node.value = "$parameter$args"; _ }; }; ] ) -> Some { Call.callee = process_callee; arguments = List.map process_arguments ~f:(fun value -> { Call.Argument.value; name = None }); } | _ -> SpecialCallResolution.redirect ~resolution { Call.callee; arguments } let redirect_special_calls ~resolution call = match transform_special_calls ~resolution call with | Some call -> call | None -> Annotated.Call.redirect_special_calls ~resolution call let resolve_recognized_callees ~resolution ~call_indexer ~callee ~return_type ~callee_type = (* Special treatment for a set of hardcoded decorators returning callable classes. *) match Node.value callee, callee_type with | ( _, Type.Parametric { name = "BoundMethod"; parameters = [Single (Parametric { name; _ }); Single implementing_class]; } ) when Set.mem Recognized.allowlisted_callable_class_decorators name -> resolve_callee_from_defining_expression ~resolution ~call_indexer ~callee ~return_type ~implementing_class | Expression.Name (Name.Attribute { base; _ }), Parametric { name; _ } when Set.mem Recognized.allowlisted_callable_class_decorators name -> (* Because of the special class, we don't get a bound method & lose the self argument for non-classmethod LRU cache wrappers. Reconstruct self in this case. *) resolve_ignoring_optional ~resolution base |> fun implementing_class -> resolve_callee_from_defining_expression ~resolution ~call_indexer ~callee ~return_type ~implementing_class | Expression.Name name, _ when is_all_names (Node.value callee) && Type.Set.mem SpecialCallResolution.recognized_callable_target_types callee_type -> Ast.Expression.name_to_reference name >>| Reference.show >>| fun name -> let collapse_tito = collapse_tito ~resolution ~callee ~callable_type:callee_type in CallCallees.create ~call_targets: [ CallTargetIndexer.create_target call_indexer ~implicit_self:false ~implicit_dunder_call:false ~collapse_tito (`Function name); ] ~return_type () | _ -> None let resolve_callee_ignoring_decorators ~resolution ~call_indexer ~collapse_tito callee = let global_resolution = Resolution.global_resolution resolution in let open UnannotatedGlobalEnvironment in match Node.value callee with | Expression.Name name when is_all_names (Node.value callee) -> ( (* Resolving expressions that do not reference local variables or parameters. *) let name = Ast.Expression.name_to_reference_exn name in match GlobalResolution.resolve_exports global_resolution name with | Some (ResolvedReference.ModuleAttribute { export = ResolvedReference.Exported (Module.Export.Name.Define _); remaining = []; _ }) -> Some (CallTargetIndexer.create_target call_indexer ~implicit_self:false ~implicit_dunder_call:false ~collapse_tito (`Function (Reference.show name))) | Some (ResolvedReference.ModuleAttribute { from; name; export = ResolvedReference.Exported Module.Export.Name.Class; remaining = [attribute]; _; }) -> ( let class_name = Reference.create ~prefix:from name |> Reference.show in GlobalResolution.class_definition global_resolution (Type.Primitive class_name) >>| Node.value >>| ClassSummary.attributes >>= Identifier.SerializableMap.find_opt attribute >>| Node.value >>= function | { kind = Method { static; _ }; _ } -> Some (CallTargetIndexer.create_target call_indexer ~implicit_self:(not static) ~implicit_dunder_call:false ~collapse_tito (`Method { Target.class_name; method_name = attribute })) | _ -> None) | _ -> None) | Expression.Name (Name.Attribute { base; attribute; _ }) -> ( (* Resolve `base.attribute` by looking up the type of `base`. *) match resolve_ignoring_optional ~resolution base with | Type.Primitive class_name | Type.Parametric { name = "type"; parameters = [Single (Type.Primitive class_name)] } -> ( GlobalResolution.class_definition global_resolution (Type.Primitive class_name) >>| Node.value >>| ClassSummary.attributes >>= Identifier.SerializableMap.find_opt attribute >>| Node.value >>= function | { kind = Method _; _ } -> Some (CallTargetIndexer.create_target call_indexer ~implicit_self:true ~implicit_dunder_call:false ~collapse_tito (`Method { Target.class_name; method_name = attribute })) | _ -> None) | _ -> None) | _ -> None let resolve_regular_callees ~resolution ~call_indexer ~return_type ~callee = let callee_type = resolve_ignoring_optional ~resolution callee in let recognized_callees = resolve_recognized_callees ~resolution ~call_indexer ~callee ~return_type ~callee_type |> Option.value ~default:(CallCallees.create_unresolved return_type) in if CallCallees.is_partially_resolved recognized_callees then recognized_callees else let callee_kind = callee_kind ~resolution callee callee_type in let collapse_tito = collapse_tito ~resolution ~callee ~callable_type:callee_type in let calleees_from_type = resolve_callees_from_type ~resolution ~call_indexer ~return_type ~callee_kind ~collapse_tito callee_type in if CallCallees.is_partially_resolved calleees_from_type then calleees_from_type else resolve_callee_ignoring_decorators ~resolution ~call_indexer ~collapse_tito callee >>| (fun target -> CallCallees.create ~call_targets:[target] ~return_type ()) |> Option.value ~default:(CallCallees.create_unresolved return_type) let resolve_callees ~resolution ~call_indexer ~call:({ Call.callee; arguments } as call) = let return_type = Expression.Call call |> Node.create_with_default_location |> Resolution.resolve_expression_to_type resolution in let higher_order_parameter = let get_higher_order_function_targets index { Call.Argument.value = argument; _ } = match resolve_regular_callees ~resolution ~call_indexer ~return_type:Type.none ~callee:argument with | { CallCallees.call_targets = _ :: _ as regular_targets; _ } -> let return_type = Expression.Call { callee = argument; arguments = [] } |> Node.create_with_default_location |> Resolution.resolve_expression_to_type resolution in Some { HigherOrderParameter.index; call_targets = regular_targets; return_type } | _ -> None in List.find_mapi arguments ~f:get_higher_order_function_targets in let regular_callees = resolve_regular_callees ~resolution ~call_indexer ~return_type ~callee in { regular_callees with higher_order_parameter } let get_property_defining_parents ~resolution ~base_annotation ~attribute = let rec get_defining_parents annotation = match annotation with | Type.Union annotations | Type.Variable { Type.Variable.Unary.constraints = Type.Variable.Explicit annotations; _ } -> List.concat_map annotations ~f:get_defining_parents | _ -> ( match defining_attribute ~resolution annotation attribute with | Some property when Annotated.Attribute.property property -> [Annotated.Attribute.parent property |> Reference.create |> Option.some] | _ -> [None]) in base_annotation |> strip_meta |> strip_optional |> get_defining_parents type attribute_access_properties = { property_targets: Target.t list; is_attribute: bool; } let resolve_attribute_access_properties ~resolution ~base_annotation ~attribute ~setter = let defining_parents = get_property_defining_parents ~resolution ~base_annotation ~attribute in let property_of_parent = function | None -> [] | Some parent -> let property_targets = if Type.is_meta base_annotation then [Target.create_method (Reference.create ~prefix:parent attribute)] else let callee = Reference.create ~prefix:parent attribute in compute_indirect_targets ~resolution ~receiver_type:base_annotation callee in if setter then let to_setter target = match target with | `OverrideTarget { Target.class_name; method_name } -> `OverrideTarget { Target.class_name; method_name = method_name ^ "$setter" } | `Method { Target.class_name; method_name } -> `Method { Target.class_name; method_name = method_name ^ "$setter" } | _ -> target in List.map property_targets ~f:to_setter else property_targets in let property_targets = List.concat_map ~f:property_of_parent defining_parents in let is_attribute = List.exists ~f:Option.is_none defining_parents || List.is_empty defining_parents in { property_targets; is_attribute } let as_global_reference ~resolution expression = match Node.value expression with | Expression.Name (Name.Identifier identifier) -> let reference = Reference.delocalize (Reference.create identifier) in if Resolution.is_global resolution ~reference then Some reference else None | Name name -> ( name_to_reference name >>= fun reference -> GlobalResolution.resolve_exports (Resolution.global_resolution resolution) reference >>= function | UnannotatedGlobalEnvironment.ResolvedReference.ModuleAttribute { from; name; remaining = []; _ } -> Some (Reference.combine from (Reference.create name)) | _ -> None) | _ -> None let resolve_attribute_access_global_targets ~resolution ~base_annotation ~base ~attribute ~special = let expression = Expression.Name (Name.Attribute { Name.Attribute.base; attribute; special }) |> Node.create_with_default_location in match as_global_reference ~resolution expression with | Some global -> [global] | None -> let global_resolution = Resolution.global_resolution resolution in let rec find_targets targets = function | Type.Union annotations -> List.fold ~init:targets ~f:find_targets annotations | Parametric { name = "type"; parameters = [Single annotation] } -> (* Access on a class, i.e `Foo.bar`, translated into `Foo.__class__.bar`. *) let parent = let attribute = Type.split annotation |> fst |> Type.primitive_name >>= GlobalResolution.attribute_from_class_name ~transitive:true ~resolution:global_resolution ~name:attribute ~instantiated:annotation in match attribute with | Some attribute when Annotated.Attribute.defined attribute -> Type.Primitive (Annotated.Attribute.parent attribute) |> Type.class_name | _ -> Type.class_name annotation in let attribute = Format.sprintf "__class__.%s" attribute in let target = Reference.create ~prefix:parent attribute in target :: targets | annotation -> (* Access on an instance, i.e `self.foo`. *) let parents = let successors = GlobalResolution.class_metadata (Resolution.global_resolution resolution) annotation >>| (fun { ClassMetadataEnvironment.successors; _ } -> successors) |> Option.value ~default:[] |> List.map ~f:(fun name -> Type.Primitive name) in annotation :: successors in let add_target targets parent = let target = Reference.create ~prefix:(Type.class_name parent) attribute in target :: targets in List.fold ~init:targets ~f:add_target parents in find_targets [] base_annotation let resolve_attribute_access ~resolution ~call_indexer ~base ~attribute ~special ~setter = let base_annotation = resolve_ignoring_optional ~resolution base in let { property_targets; is_attribute } = resolve_attribute_access_properties ~resolution ~base_annotation ~attribute ~setter in let property_targets = List.map ~f: (CallTargetIndexer.create_target call_indexer ~implicit_self:true ~implicit_dunder_call:false ~collapse_tito:true) property_targets in let global_targets = resolve_attribute_access_global_targets ~resolution ~base_annotation ~base ~attribute ~special |> List.map ~f:Target.create_object |> List.filter ~f:FixpointState.has_model |> List.map ~f: (CallTargetIndexer.create_target call_indexer ~implicit_self:false ~implicit_dunder_call:false ~collapse_tito:true) in let return_type = if setter then Type.none else Expression.Name (Name.Attribute { Name.Attribute.base; attribute; special }) |> Node.create_with_default_location |> Resolution.resolve_expression_to_type resolution in { AttributeAccessCallees.property_targets; global_targets; return_type; is_attribute } let resolve_identifier ~resolution ~call_indexer ~identifier = Expression.Name (Name.Identifier identifier) |> Node.create_with_default_location |> as_global_reference ~resolution >>| Target.create_object |> Option.filter ~f:FixpointState.has_model >>| fun global -> { IdentifierCallees.global_targets = [ CallTargetIndexer.create_target call_indexer ~implicit_self:false ~implicit_dunder_call:false ~collapse_tito:true global; ]; } (* This is a bit of a trick. The only place that knows where the local annotation map keys is the fixpoint (shared across the type check and additional static analysis modules). By having a fixpoint that always terminates (by having a state = unit), we re-use the fixpoint id's without having to hackily recompute them. *) module DefineCallGraphFixpoint (Context : sig val global_resolution : GlobalResolution.t val local_annotations : LocalAnnotationMap.ReadOnly.t option val parent : Reference.t option val callees_at_location : UnprocessedLocationCallees.t Location.Table.t val call_indexer : CallTargetIndexer.t end) = struct type assignment_target = { location: Location.t } type visitor_t = { resolution: Resolution.t; assignment_target: assignment_target option; } let call_indexer = Context.call_indexer module NodeVisitor = struct type nonrec t = visitor_t let expression_visitor ({ resolution; assignment_target } as state) { Node.value; location } = CallTargetIndexer.generate_fresh_indices call_indexer; let register_targets ~expression_identifier ?(location = location) callees = Location.Table.update Context.callees_at_location location ~f:(function | None -> UnprocessedLocationCallees.singleton ~expression_identifier ~callees | Some existing_callees -> UnprocessedLocationCallees.add existing_callees ~expression_identifier ~callees) in let () = match value with | Expression.Call call -> let call = redirect_special_calls ~resolution call in resolve_callees ~resolution ~call_indexer ~call |> ExpressionCallees.from_call |> register_targets ~expression_identifier:(call_identifier call) | Expression.Name (Name.Attribute { Name.Attribute.base; attribute; special }) -> let setter = match assignment_target with | Some { location = assignment_target_location } -> Location.equal assignment_target_location location | None -> false in resolve_attribute_access ~resolution ~call_indexer ~base ~attribute ~special ~setter |> ExpressionCallees.from_attribute_access |> register_targets ~expression_identifier:attribute | Expression.Name (Name.Identifier identifier) -> resolve_identifier ~resolution ~call_indexer ~identifier >>| ExpressionCallees.from_identifier >>| register_targets ~expression_identifier:identifier |> ignore | Expression.ComparisonOperator comparison -> ( match ComparisonOperator.override ~location comparison with | Some { Node.value = Expression.Call call; _ } -> let call = redirect_special_calls ~resolution call in resolve_callees ~resolution ~call_indexer ~call |> ExpressionCallees.from_call |> register_targets ~expression_identifier:(call_identifier call) | _ -> ()) | Expression.FormatString substrings -> List.iter substrings ~f:(function | Substring.Literal _ -> () | Substring.Format ({ Node.location = expression_location; _ } as expression) -> let { CallCallees.call_targets; _ } = let callee = let method_name = Annotated.Call.resolve_stringify_call ~resolution expression in { Node.value = Expression.Name (Name.Attribute { base = expression; attribute = method_name; special = false }); location = expression_location; } in CallTargetIndexer.generate_fresh_indices call_indexer; resolve_regular_callees ~resolution ~call_indexer ~return_type:Type.string ~callee in if not (List.is_empty call_targets) then let callees = ExpressionCallees.from_format_string { FormatStringCallees.call_targets } in register_targets ~expression_identifier:DefineCallGraph.format_string_expression_identifier ~location:expression_location callees) | _ -> () in (* Special-case `getattr()` and `setattr()` for the taint analysis. *) let () = match value with | Expression.Call { callee = { Node.value = Name (Name.Identifier "getattr"); _ }; arguments = [ { Call.Argument.value = base; _ }; { Call.Argument.value = { Node.value = Expression.Constant (Constant.String { StringLiteral.value = attribute; _ }); _; }; _; }; { Call.Argument.value = _; _ }; ]; } -> resolve_attribute_access ~resolution ~call_indexer ~base ~attribute ~special:false ~setter:false |> ExpressionCallees.from_attribute_access |> register_targets ~expression_identifier:attribute | Expression.Call { callee = { Node.value = Name (Name.Attribute { base = { Node.value = Name (Name.Identifier "object"); _ }; attribute = "__setattr__"; _; }); _; }; arguments = [ { Call.Argument.value = self; name = None }; { Call.Argument.value = { Node.value = Expression.Constant (Constant.String { value = attribute; kind = String }); _; }; name = None; }; { Call.Argument.value = _; name = None }; ]; } -> resolve_attribute_access ~resolution ~call_indexer ~base:self ~attribute ~special:true ~setter:true |> ExpressionCallees.from_attribute_access |> register_targets ~expression_identifier:attribute | _ -> () in state let statement_visitor state _ = state let generator_visitor ({ resolution; _ } as state) generator = (* Since generators create variables that Pyre sees as scoped within the generator, handle them by adding the generator's bindings to the resolution. *) { state with resolution = Resolution.resolve_assignment resolution (Ast.Statement.Statement.generator_assignment generator); } let node state = function | Visit.Expression expression -> expression_visitor state expression | Visit.Statement statement -> statement_visitor state statement | Visit.Generator generator -> generator_visitor state generator | _ -> state let visit_statement_children _ statement = match Node.value statement with | Statement.Assign _ | Statement.Define _ | Statement.Class _ -> false | _ -> true let visit_format_string_children _ _ = true end module CalleeVisitor = Visit.MakeNodeVisitor (NodeVisitor) include Fixpoint.Make (struct type t = unit [@@deriving show] let bottom = () let less_or_equal ~left:_ ~right:_ = true let join _ _ = () let widen ~previous:_ ~next:_ ~iteration:_ = () let forward_statement ~resolution ~statement = match Node.value statement with | Statement.Assign { Assign.target; value; _ } -> CalleeVisitor.visit_expression ~state: (ref { resolution; assignment_target = Some { location = Node.location target } }) target; CalleeVisitor.visit_expression ~state:(ref { resolution; assignment_target = None }) value | _ -> CalleeVisitor.visit_statement ~state:(ref { resolution; assignment_target = None }) statement let forward ~statement_key _ ~statement = let resolution = TypeCheck.resolution_with_key ~global_resolution:Context.global_resolution ~local_annotations:Context.local_annotations ~parent:Context.parent ~statement_key (module TypeCheck.DummyContext) in forward_statement ~resolution ~statement let backward ~statement_key:_ _ ~statement:_ = () end) end let call_graph_of_define ~environment ~define:({ Define.signature = { Define.Signature.name; parent; _ }; _ } as define) = let timer = Timer.start () in let callees_at_location = Location.Table.create () in let module DefineFixpoint = DefineCallGraphFixpoint (struct let global_resolution = TypeEnvironment.ReadOnly.global_resolution environment let local_annotations = TypeEnvironment.ReadOnly.get_local_annotations environment name let parent = parent let callees_at_location = callees_at_location let call_indexer = CallTargetIndexer.create () end) in (* Handle parameters. *) let () = let resolution = TypeCheck.resolution (TypeEnvironment.ReadOnly.global_resolution environment) (module TypeCheck.DummyContext) in List.iter define.Ast.Statement.Define.signature.parameters ~f:(fun { Node.value = { Parameter.value; _ }; _ } -> Option.iter value ~f:(fun value -> DefineFixpoint.CalleeVisitor.visit_expression ~state:(ref { DefineFixpoint.resolution; assignment_target = None }) value)) in DefineFixpoint.forward ~cfg:(Cfg.create define) ~initial:() |> ignore; let call_graph = Location.Table.to_alist callees_at_location |> List.map ~f:(fun (location, unprocessed_callees) -> match String.Map.Tree.to_alist unprocessed_callees with | [] -> failwith "unreachable" | [(_, callees)] -> location, LocationCallees.Singleton (ExpressionCallees.deduplicate callees) | _ -> ( location, LocationCallees.Compound (Core.String.Map.Tree.map ~f:ExpressionCallees.deduplicate unprocessed_callees) )) |> List.filter ~f:(fun (_, callees) -> match callees with | LocationCallees.Singleton singleton -> not (ExpressionCallees.is_empty_attribute_access_callees singleton) | LocationCallees.Compound compound -> Core.String.Map.Tree.exists compound ~f:(fun callees -> not (ExpressionCallees.is_empty_attribute_access_callees callees))) |> Location.Map.of_alist_exn in Statistics.performance ~randomly_log_every:1000 ~always_log_time_threshold:1.0 (* Seconds *) ~name:"Call graph built" ~section:`DependencyGraph ~normals:["callable", Reference.show name] ~timer (); call_graph module SharedMemory = struct include Memory.WithCache.Make (Target.CallableKey) (struct type t = LocationCallees.t Location.Map.Tree.t let prefix = Prefix.make () let description = "call graphs of defines" let unmarshall value = Marshal.from_string value 0 end) let add ~callable ~call_graph = add callable (Location.Map.to_tree call_graph) let get ~callable = get callable >>| Location.Map.of_tree let get_or_compute ~callable ~environment ~define = match get ~callable with | Some map -> map | None -> let call_graph = call_graph_of_define ~environment ~define in add ~callable ~call_graph; call_graph let remove callables = KeySet.of_list callables |> remove_batch end let create_callgraph ?(use_shared_memory = false) ~environment ~source = let fold_defines dependencies = function | { Node.value = define; _ } when Define.is_overloaded_function define -> dependencies | define -> let call_graph_of_define = if use_shared_memory then SharedMemory.get_or_compute ~callable:(Target.create define) ~environment ~define:(Node.value define) else call_graph_of_define ~environment ~define:(Node.value define) in let non_object_target = function | `Object _ -> false | _ -> true in Location.Map.data call_graph_of_define |> List.concat_map ~f:LocationCallees.all_targets |> List.filter ~f:non_object_target |> List.dedup_and_sort ~compare:Target.compare |> fun callees -> Target.CallableMap.set dependencies ~key:(Target.create define) ~data:callees in Preprocessing.defines ~include_nested:true ~include_toplevels:true source |> List.fold ~init:Target.CallableMap.empty ~f:fold_defines