Skip to content

Commit 15e4456

Browse files
arthaudmeta-codesync[bot]
authored andcommitted
Export function-like class attributes as function definitions
Summary: The pyrefly-based Pysa analysis currently does not see synthesized functions, such as the `__init__` and `__hash__` methods generated by `dataclass`. We are also blind to methods declared with `foo: Callable[.., int]` in the class body. This diff updates both pyrefly and pysa to consider those as functions, introducing a new `LocalFunctionId` kind `ClassField`. Reviewed By: tianhan0 Differential Revision: D84843696 fbshipit-source-id: efb1e36d290fbd40160dd3a1e94a74bca1edf081
1 parent 5b82f0d commit 15e4456

23 files changed

+585
-345
lines changed

source/analysis/pyrePysaEnvironment.ml

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,46 @@ module PysaClassSummary = struct
218218
|> Ast.Identifier.SerializableMap.data
219219
end
220220

221+
module AstResult = struct
222+
type 'a t =
223+
| Some of 'a
224+
| ParseError (* callable in a module that failed to parse *)
225+
| TestFile (* callable in a module marked with is_test = true *)
226+
| Synthesized (* callable in a synthesized class or function *)
227+
| Pyre1NotFound (* callable not found - only raised when using pyre1 *)
228+
229+
let to_option = function
230+
| ParseError -> None
231+
| TestFile -> None
232+
| Synthesized -> None
233+
| Pyre1NotFound -> None
234+
| Some ast -> Some ast
235+
236+
237+
let value_exn ~message = function
238+
| Some value -> value
239+
| ParseError -> Format.sprintf "%s (reason: parser error)" message |> failwith
240+
| TestFile -> Format.sprintf "%s (reason: within a test file)" message |> failwith
241+
| Synthesized -> Format.sprintf "%s (reason: synthesized function)" message |> failwith
242+
| Pyre1NotFound -> Format.sprintf "%s (reason: not found)" message |> failwith
243+
244+
245+
let map ~f = function
246+
| Some ast -> Some (f ast)
247+
| ParseError -> ParseError
248+
| TestFile -> TestFile
249+
| Synthesized -> Synthesized
250+
| Pyre1NotFound -> Pyre1NotFound
251+
252+
253+
let map_node ~f = function
254+
| Some { Ast.Node.value = ast; location } -> Some { Ast.Node.value = f ast; location }
255+
| ParseError -> ParseError
256+
| TestFile -> TestFile
257+
| Synthesized -> Synthesized
258+
| Pyre1NotFound -> Pyre1NotFound
259+
end
260+
221261
let absolute_source_path_of_qualifier ~lookup_source read_only_type_environment =
222262
let source_code_api =
223263
read_only_type_environment |> TypeEnvironment.ReadOnly.get_untracked_source_code_api
@@ -448,7 +488,9 @@ module ReadOnly = struct
448488
let get_class_summary api = global_resolution api |> GlobalResolution.get_class_summary
449489

450490
let get_class_decorators_opt api class_name =
451-
get_class_summary api class_name >>| Ast.Node.value >>| fun { decorators; _ } -> decorators
491+
match get_class_summary api class_name with
492+
| Some { Ast.Node.value = { PyreClassSummary.decorators; _ }; _ } -> AstResult.Some decorators
493+
| None -> AstResult.Pyre1NotFound
452494

453495

454496
let get_class_attributes api ~include_generated_attributes ~only_simple_assignments class_name =

source/analysis/pyrePysaEnvironment.mli

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,23 @@ module PysaClassSummary : sig
102102
val get_attributes : t -> PyreClassSummary.Attribute.t list
103103
end
104104

105+
module AstResult : sig
106+
type 'a t =
107+
| Some of 'a
108+
| ParseError (* callable in a module that failed to parse *)
109+
| TestFile (* callable in a module marked with is_test = true *)
110+
| Synthesized (* callable in a synthesized class or function *)
111+
| Pyre1NotFound (* callable not found - only raised when using pyre1 *)
112+
113+
val to_option : 'a t -> 'a option
114+
115+
val value_exn : message:string -> 'a t -> 'a
116+
117+
val map : f:('a -> 'b) -> 'a t -> 'b t
118+
119+
val map_node : f:('a -> 'b) -> 'a Ast.Node.t t -> 'b Ast.Node.t t
120+
end
121+
105122
module ReadWrite : sig
106123
type t
107124

@@ -169,7 +186,7 @@ module ReadOnly : sig
169186

170187
val get_class_summary : t -> string -> PysaClassSummary.t option
171188

172-
val get_class_decorators_opt : t -> string -> Ast.Expression.t list option
189+
val get_class_decorators_opt : t -> string -> Ast.Expression.t list AstResult.t
173190

174191
val get_class_attributes
175192
: t ->

source/interprocedural/callGraph.ml

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ open Expression
2323
open Pyre
2424
module TaintAccessPath = Analysis.TaintAccessPath
2525
module PyrePysaLogic = Analysis.PyrePysaLogic
26+
module AstResult = PyrePysaApi.AstResult
2627

2728
module JsonHelper = struct
2829
let add_optional name value to_json bindings =
@@ -192,7 +193,8 @@ module CallableToDecoratorsMap = struct
192193
let collect_decorators ~callables_to_definitions_map callable =
193194
callable
194195
|> Option.some_if (Target.is_normal callable)
195-
>>= Target.CallablesSharedMemory.ReadOnly.get_signature callables_to_definitions_map
196+
>>| Target.CallablesSharedMemory.ReadOnly.get_signature callables_to_definitions_map
197+
>>= AstResult.to_option
196198
>>= fun { Target.CallableSignature.decorators; location = define_location; _ } ->
197199
let decorators = decorators |> List.filter ~f:filter_decorator |> List.rev in
198200
if List.is_empty decorators then
@@ -2068,6 +2070,7 @@ struct
20682070
let resolve_module_path = Option.value ~default:(fun _ -> None) resolve_module_path in
20692071
callable
20702072
|> Target.CallablesSharedMemory.ReadOnly.get_qualifier callables_to_definitions_map
2073+
|> AstResult.to_option
20712074
>>= resolve_module_path
20722075
>>| (function
20732076
| { RepositoryPath.filename = Some filename; _ } ->
@@ -4300,6 +4303,7 @@ let resolve_callees
43004303
Target.CallablesSharedMemory.ReadOnly.get_signature
43014304
callables_to_definitions_map
43024305
callee.target
4306+
|> AstResult.to_option
43034307
>>| (fun { Target.CallableSignature.parameters; is_stub; _ } ->
43044308
is_stub
43054309
|| not (List.exists parameters ~f:(parameter_has_annotation callable_class)))
@@ -5530,10 +5534,7 @@ module HigherOrderCallGraph = struct
55305534
Context.callables_to_definitions_map
55315535
target
55325536
with
5533-
| None ->
5534-
log "Cannot find define for callable `%a`" Target.pp_pretty_with_kind target;
5535-
None
5536-
| Some { Target.CallableSignature.is_stub; parameters; _ } ->
5537+
| AstResult.Some { Target.CallableSignature.is_stub; parameters; _ } ->
55375538
if is_stub then
55385539
let () = log "Callable `%a` is a stub" Target.pp_pretty_with_kind target in
55395540
None
@@ -5542,6 +5543,9 @@ module HigherOrderCallGraph = struct
55425543
(parameters
55435544
|> TaintAccessPath.normalize_parameters
55445545
|> List.map ~f:(fun { TaintAccessPath.NormalizedParameter.root; _ } -> root))
5546+
| _ ->
5547+
log "Cannot find define for callable `%a`" Target.pp_pretty_with_kind target;
5548+
None
55455549
in
55465550
let create_parameter_target_excluding_args_kwargs (parameter_target, (_, argument_matches)) =
55475551
match argument_matches, parameter_target with
@@ -5692,7 +5696,7 @@ module HigherOrderCallGraph = struct
56925696
let stub_targets =
56935697
List.filter_map call_targets_from_callee ~f:(fun { CallTarget.target; _ } ->
56945698
let is_stub =
5695-
Target.CallablesSharedMemory.ReadOnly.is_stub
5699+
Target.CallablesSharedMemory.ReadOnly.is_stub_like
56965700
Context.callables_to_definitions_map
56975701
target
56985702
|> Option.value ~default:false
@@ -6338,6 +6342,7 @@ module HigherOrderCallGraph = struct
63386342
(to avoid bloat), use this API to query the definition. *)
63396343
Target.CallablesSharedMemory.ReadOnly.get_captures
63406344
Context.callables_to_definitions_map
6345+
|> AstResult.to_option
63416346
| _ ->
63426347
Format.asprintf
63436348
"Expect a single `define_target` but got `[%s]`"

source/interprocedural/callGraphFixpoint.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ module CallGraphAnalysis = struct
106106
callable
107107
|> Target.strip_parameters
108108
|> Target.CallablesSharedMemory.ReadOnly.get_define callables_to_definitions_map
109-
|> Option.value_exn
109+
|> PyrePysaApi.AstResult.value_exn
110110
~message:(Format.asprintf "Found no definition for `%a`" Target.pp_pretty callable)
111111
in
112112
if Ast.Statement.Define.is_stub (Ast.Node.value define) then

source/interprocedural/fetchCallables.ml

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,14 +150,33 @@ let from_qualifier_with_pyrefly ~pyrefly_api ~qualifier =
150150
qualifier
151151
in
152152
let is_stub_module = PyreflyApi.ReadOnly.is_stub_qualifier pyrefly_api qualifier in
153+
let is_test_module = PyreflyApi.ReadOnly.is_test_qualifier pyrefly_api qualifier in
153154
let add_target result define_name =
154155
let target = Target.from_define_name ~pyrefly_api define_name in
155-
let { PyreflyApi.CallableMetadata.is_stub = is_stub_define; is_toplevel; is_class_toplevel; _ } =
156+
let {
157+
PyreflyApi.CallableMetadata.is_stub = is_stub_define;
158+
is_toplevel;
159+
is_class_toplevel;
160+
is_def_statement;
161+
_;
162+
}
163+
=
156164
PyreflyApi.ReadOnly.get_callable_metadata pyrefly_api define_name
157165
in
158166
if is_stub_module && (is_toplevel || is_class_toplevel) then
167+
(* Ignore top level define for stub modules (i.e, `.pyi`) *)
159168
result
160-
else if is_stub_define then
169+
else if
170+
is_stub_define
171+
|| is_test_module
172+
|| ((not is_def_statement) && (not is_toplevel) && not is_class_toplevel)
173+
then
174+
(* Considered as stub:
175+
* - Stub functions, i.e when the body is an ellipsis `def foo(): ...`
176+
* - Functions in a module considered a unit test module
177+
* - Synthesized functions that we don't have the code for (for instance,
178+
* generated `__init__` of a dataclass)
179+
*)
161180
{ result with stubs = target :: result.stubs }
162181
else
163182
(* TODO(T225700656): For now, all modules are considered internal. We could potentially use

source/interprocedural/globalConstants.ml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ open Ast
1010
open Statement
1111
open Expression
1212
module PyrePysaLogic = Analysis.PyrePysaLogic
13+
module AstResult = PyrePysaApi.AstResult
1314

1415
module Heap = struct
1516
type t = StringLiteral.t Reference.Map.t [@@deriving show, equal]
@@ -64,6 +65,7 @@ module Heap = struct
6465
|> PyrePysaApi.ReadOnly.get_qualifier_top_level_define_name pyre_api
6566
|> Target.create_function
6667
|> Target.CallablesSharedMemory.ReadOnly.get_define callables_to_definitions_map
68+
|> AstResult.to_option
6769
>>| (fun { Target.CallablesSharedMemory.DefineAndQualifier.define; _ } -> define)
6870
>>| Ast.Node.value
6971
>>| (fun { Ast.Statement.Define.body; _ } -> body)

source/interprocedural/pyrePysaApi.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module ScalarTypeProperties = Pyre1Api.ScalarTypeProperties
1414
module ClassNamesFromType = Pyre1Api.ClassNamesFromType
1515
module PysaType = Pyre1Api.PysaType
1616
module PyreClassSummary = Pyre1Api.PyreClassSummary
17+
module AstResult = Pyre1Api.AstResult
1718

1819
module PysaClassSummary = struct
1920
type t =

source/interprocedural/pyrePysaApi.mli

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ module ScalarTypeProperties = Analysis.PyrePysaEnvironment.ScalarTypeProperties
1515
module ClassNamesFromType = Analysis.PyrePysaEnvironment.ClassNamesFromType
1616
module PysaType = Analysis.PyrePysaEnvironment.PysaType
1717
module PyreClassSummary = Analysis.ClassSummary
18+
module AstResult = Analysis.PyrePysaEnvironment.AstResult
1819

1920
(* Abstraction for information about a class, provided from Pyre1 or Pyrefly and used by Pysa. See
2021
`ReadOnly.ClassSummary` for more functions. *)
@@ -122,7 +123,7 @@ module ReadOnly : sig
122123

123124
val get_class_summary : t -> string -> PysaClassSummary.t option
124125

125-
val get_class_decorators_opt : t -> string -> Ast.Expression.t list option
126+
val get_class_decorators_opt : t -> string -> Ast.Expression.t list AstResult.t
126127

127128
val get_class_attributes
128129
: t ->

0 commit comments

Comments
 (0)