[nullsafe][refactor] Introduce ThirdPartyMethod.t

Summary:
Currently, the notion of a third party method signature (corresponding to
a single record in a .sig file) is scattered between unique_repr and
nullability.

Logically, ThirdPartyMethod.t should not "know" about unique_repr
because this is a detail needed only for searching for a method
definition in `.sig` storage.

This diff:
1. Introduces ThirdPartyMethod.t which exposes exactly information
stored in .sig file. We are going to use this abstraction more in follow
up diffs.
2. Moves functionality operating with `unique_repr` to the module
responsible for searching in the repository.
3. Deletes `ThirdPartyMethod.nullability` abstraction - we don't need it as we can just store ThirdPartyMethod.t where was previously `nullability` stored.
4. Introduces `to_canonical_string` method that prints back third party
method in a format needed for `.sig` file, and a test ensuring that
parsing works back and forth consistently. We are going to use this
method in follow up diffs as well.

Reviewed By: artempyanykh

Differential Revision: D22950706

fbshipit-source-id: a0e72ccdb
master
Mitya Lyubarskiy 4 years ago committed by Facebook GitHub Bot
parent fe617afa49
commit 014f330037

@ -7,11 +7,39 @@
open! IStd
module Hashtbl = Caml.Hashtbl
type signature_info = {filename: string; line_number: int; nullability: ThirdPartyMethod.nullability}
type signature_info = {filename: string; line_number: int; signature: ThirdPartyMethod.t}
type unique_repr =
{ class_name: ThirdPartyMethod.fully_qualified_type
; method_name: ThirdPartyMethod.method_name
; param_types: ThirdPartyMethod.fully_qualified_type list }
[@@deriving sexp]
let pp_unique_repr fmt signature = Sexp.pp fmt (sexp_of_unique_repr signature)
let unique_repr_of_signature ThirdPartyMethod.{class_name; method_name; params} =
{class_name; method_name; param_types= List.map params ~f:(fun (param_type, _) -> param_type)}
let java_type_to_string java_type =
Pp.string_of_pp (JavaSplitName.pp_type_verbosity ~verbose:true) java_type
let unique_repr_of_java_proc_name java_proc_name =
let class_name = Procname.Java.get_class_name java_proc_name in
let method_name =
if Procname.Java.is_constructor java_proc_name then ThirdPartyMethod.Constructor
else ThirdPartyMethod.Method (Procname.Java.get_method java_proc_name)
in
let param_types =
Procname.Java.get_parameters java_proc_name |> List.map ~f:java_type_to_string
in
{class_name; method_name; param_types}
type storage = {signature_map: signature_map; filenames: string list}
and signature_map = (ThirdPartyMethod.unique_repr, signature_info) Hashtbl.t
and signature_map = (unique_repr, signature_info) Hashtbl.t
let create_storage () = {signature_map= Hashtbl.create 1; filenames= []}
@ -42,9 +70,10 @@ let parse_line_and_add_to_storage signature_map ~filename ~line_index line =
if is_whitespace_or_comment line then Ok signature_map
else
ThirdPartyMethod.parse line
>>= fun (signature, nullability) ->
>>= fun signature ->
let key = unique_repr_of_signature signature in
Ok
( Hashtbl.add signature_map signature {filename; line_number= line_index + 1; nullability} ;
( Hashtbl.add signature_map key {filename; line_number= line_index + 1; signature} ;
signature_map )

@ -11,7 +11,21 @@ open! IStd
type signature_info =
{ filename: string (** File where the particular signature is stored *)
; line_number: int (** Line number with this signature *)
; nullability: ThirdPartyMethod.nullability }
; signature: ThirdPartyMethod.t }
(** The minimum information that is needed to _uniquely_ identify the method. That why we don't
- include e.g. return type, access quilifiers, or whether the method is static (because Java
- overload resolution rules ignore these things). In contrast, parameter types are essential,
- because Java allows several methods with different types. *)
type unique_repr =
{ class_name: ThirdPartyMethod.fully_qualified_type
; method_name: ThirdPartyMethod.method_name
; param_types: ThirdPartyMethod.fully_qualified_type list }
val pp_unique_repr : Format.formatter -> unique_repr -> unit
val unique_repr_of_java_proc_name : Procname.Java.t -> unique_repr
type storage
@ -26,7 +40,7 @@ val add_from_signature_file :
storage -> filename:string -> lines:string list -> (storage, file_parsing_error) result
(** Parse the information from the signature file, and add it to the storage *)
val find_nullability_info : storage -> ThirdPartyMethod.unique_repr -> signature_info option
val find_nullability_info : storage -> unique_repr -> signature_info option
(** The main method. Do we have an information about the third-party method? If we do not, or it is
not a third-party method, returns None. Otherwise returns the nullability information. *)

@ -5,23 +5,19 @@
* LICENSE file in the root directory of this source tree.
*)
open! IStd
module F = Format
open Result.Monad_infix
type fully_qualified_type = string [@@deriving sexp]
type unique_repr =
type t =
{ class_name: fully_qualified_type
; method_name: method_name
; param_types: fully_qualified_type list }
[@@deriving sexp]
; ret_nullability: type_nullability
; params: (fully_qualified_type * type_nullability) list }
and method_name = Constructor | Method of string
and fully_qualified_type = string [@@deriving sexp]
type type_nullability = Nullable | Nonnull [@@deriving sexp]
and method_name = Constructor | Method of string
type nullability = {ret_nullability: type_nullability; param_nullability: type_nullability list}
[@@deriving sexp]
and type_nullability = Nullable | Nonnull [@@deriving sexp]
type parsing_error =
| BadStructure
@ -46,33 +42,6 @@ let string_of_parsing_error = function
"Each param should have form of [@Nullable] <fully qualified type name>"
let pp_unique_repr fmt signature = Sexp.pp fmt (sexp_of_unique_repr signature)
let java_type_to_string java_type =
let package = JavaSplitName.package java_type in
let type_name = JavaSplitName.type_name java_type in
match package with
| None ->
(* Primitive type *)
type_name
| Some package ->
package ^ "." ^ type_name
let unique_repr_of_java_proc_name java_proc_name =
let class_name = Procname.Java.get_class_name java_proc_name in
let method_name =
if Procname.Java.is_constructor java_proc_name then Constructor
else Method (Procname.Java.get_method java_proc_name)
in
let param_types =
Procname.Java.get_parameters java_proc_name |> List.map ~f:java_type_to_string
in
{class_name; method_name; param_types}
let pp_nullability fmt nullability = Sexp.pp fmt (sexp_of_nullability nullability)
let nullable_annotation = "@Nullable"
let identifier_regexp = lazy (Str.regexp "[_a-zA-Z][_a-zA-Z0-9]*$")
@ -192,12 +161,26 @@ let parse str =
parse_method_name method_name_str
>>= fun method_name ->
match_after_open_brace rest
>>= fun (parsed_params, ret_nullability) ->
let param_types, param_nullability = List.unzip parsed_params in
Ok ({class_name; method_name; param_types}, {ret_nullability; param_nullability})
>>= fun (params, ret_nullability) -> Ok {class_name; method_name; ret_nullability; params}
| _ ->
Error BadStructure
let pp_parse_result fmt (unique_repr, nullability) =
F.fprintf fmt "(%a; %a)" pp_unique_repr unique_repr pp_nullability nullability
let to_canonical_string
{ class_name: fully_qualified_type
; method_name: method_name
; ret_nullability: type_nullability
; params } =
let method_name_to_string = function Constructor -> "<init>" | Method name -> name in
let param_to_string (typ, nullability) =
match nullability with Nullable -> Format.sprintf "@Nullable %s" typ | Nonnull -> typ
in
let param_list_to_string params = List.map params ~f:param_to_string |> String.concat ~sep:", " in
let ret_to_string = function Nullable -> " @Nullable" | Nonnull -> "" in
Format.sprintf "%s#%s(%s)%s" class_name
(method_name_to_string method_name)
(param_list_to_string params) (ret_to_string ret_nullability)
(* Pretty print exactly as the canonical representation, for convenience *)
let pp = Pp.of_string ~f:to_canonical_string

@ -11,39 +11,31 @@
open! IStd
(** E.g. "full.package.name.TypeName$NestedTypeName1$NestedTypeName2" *)
type fully_qualified_type = string
(** The minimum information that is needed to _uniquely_ identify the method. That why we don't
include e.g. return type, access quilifiers, or whether the method is static (because Java
overload resolution rules ignore these things). In contrast, parameter types are essential,
because Java allows several methods with different types. *)
type unique_repr =
type t =
{ class_name: fully_qualified_type
; method_name: method_name
; param_types: fully_qualified_type list }
; ret_nullability: type_nullability
; params: (fully_qualified_type * type_nullability) list }
and method_name = Constructor | Method of string
and fully_qualified_type = string [@@deriving sexp]
val unique_repr_of_java_proc_name : Procname.Java.t -> unique_repr
and method_name = Constructor | Method of string [@@deriving sexp]
val pp_unique_repr : Format.formatter -> unique_repr -> unit
type nullability = {ret_nullability: type_nullability; param_nullability: type_nullability list}
and type_nullability = Nullable | Nonnull
val pp_nullability : Format.formatter -> nullability -> unit
and type_nullability = Nullable | Nonnull [@@deriving sexp]
type parsing_error
val string_of_parsing_error : parsing_error -> string
val parse : string -> (unique_repr * nullability, parsing_error) result
val parse : string -> (t, parsing_error) result
(** Given a string representing nullability information for a given third-party method, return the
method signature and nullability of its params and return values. The string should come from a
repository storing 3rd party annotations. E.g.
["package.name.Class$NestedClass#foo(package.name.SomeClass, @Nullable package.name.OtherClass)
@Nullable"] *)
val pp_parse_result : Format.formatter -> unique_repr * nullability -> unit
val to_canonical_string : t -> string
val pp : Format.formatter -> t -> unit
(** String representation as it can be parsed via [parse]
<Class>#<method>(<params>)<ret_nullability> *)

@ -31,17 +31,17 @@ let get_unique_repr proc_name =
let java_proc_name =
match proc_name with Procname.Java java_proc_name -> Some java_proc_name | _ -> None
in
Option.map java_proc_name ~f:ThirdPartyMethod.unique_repr_of_java_proc_name
Option.map java_proc_name ~f:ThirdPartyAnnotationInfo.unique_repr_of_java_proc_name
let to_modelled_nullability ThirdPartyMethod.{ret_nullability; param_nullability} =
let to_modelled_nullability ThirdPartyMethod.{ret_nullability; params} =
let is_nullable = function
| ThirdPartyMethod.Nullable ->
true
| ThirdPartyMethod.Nonnull ->
false
in
(is_nullable ret_nullability, List.map param_nullability ~f:is_nullable)
(is_nullable ret_nullability, List.map params ~f:(fun (_, nullability) -> is_nullable nullability))
(* Some methods *)
@ -91,8 +91,8 @@ let get_modelled_annotated_signature ~is_callee_in_trust_list tenv proc_attribut
~f:
(ThirdPartyAnnotationInfo.find_nullability_info
(ThirdPartyAnnotationGlobalRepo.get_repo ()))
|> Option.map ~f:(fun ThirdPartyAnnotationInfo.{nullability; filename; line_number} ->
(to_modelled_nullability nullability, filename, line_number) )
|> Option.map ~f:(fun ThirdPartyAnnotationInfo.{signature; filename; line_number} ->
(to_modelled_nullability signature, filename, line_number) )
|> Option.value_map
(* If we found information in third-party repo, overwrite annotated signature *)
~f:(fun (modelled_nullability, filename, line_number) ->

@ -15,13 +15,23 @@ let assert_has_nullability_info ?expected_file ?expected_line storage unique_rep
| None ->
assert_failure
(F.asprintf "Expected to find info for %a, but it was not found"
ThirdPartyMethod.pp_unique_repr unique_repr)
| Some {filename; line_number; nullability} ->
assert_equal expected_nullability nullability
ThirdPartyAnnotationInfo.pp_unique_repr unique_repr)
| Some {filename; line_number; signature} ->
let expected_ret, expected_param_nullability = expected_nullability in
let expected_params =
List.zip_exn unique_repr.ThirdPartyAnnotationInfo.param_types expected_param_nullability
in
let expected_signature =
{ ThirdPartyMethod.class_name= unique_repr.ThirdPartyAnnotationInfo.class_name
; method_name= unique_repr.ThirdPartyAnnotationInfo.method_name
; ret_nullability= expected_ret
; params= expected_params }
in
assert_equal expected_signature signature
~msg:
(F.asprintf "Nullability info for %a does not match" ThirdPartyMethod.pp_unique_repr
(F.asprintf "Signature for %a does not match" ThirdPartyAnnotationInfo.pp_unique_repr
unique_repr)
~printer:(Pp.string_of_pp ThirdPartyMethod.pp_nullability) ;
~printer:(Pp.string_of_pp ThirdPartyMethod.pp) ;
Option.iter expected_file ~f:(fun expected_file ->
assert_equal expected_file filename ~msg:"Filename does not match" ) ;
Option.iter expected_line ~f:(fun expected_line ->
@ -32,10 +42,10 @@ let assert_no_info storage unique_repr =
match ThirdPartyAnnotationInfo.find_nullability_info storage unique_repr with
| None ->
()
| Some {nullability} ->
| Some {signature} ->
assert_failure
(F.asprintf "Did not expect to find nullability info for method %a, but found %a"
ThirdPartyMethod.pp_unique_repr unique_repr ThirdPartyMethod.pp_nullability nullability)
ThirdPartyAnnotationInfo.pp_unique_repr unique_repr ThirdPartyMethod.pp signature)
let add_from_annot_file_and_check_success storage ~filename ~lines =
@ -72,11 +82,11 @@ let basic_find =
(* Make sure we can find what we just stored *)
assert_has_nullability_info storage
{class_name= "a.A"; method_name= Method "foo"; param_types= ["b.B"]}
~expected_nullability:{ret_nullability= Nonnull; param_nullability= [Nonnull]}
~expected_nullability:(Nonnull, [Nonnull])
~expected_file:"test.sig" ~expected_line:1 ;
assert_has_nullability_info storage
{class_name= "b.B"; method_name= Method "bar"; param_types= ["c.C"; "d.D"]}
~expected_nullability:{ret_nullability= Nullable; param_nullability= [Nonnull; Nullable]}
~expected_nullability:(Nullable, [Nonnull; Nullable])
~expected_file:"test.sig" ~expected_line:2 ;
(* Make sure we can not find stuff we did not store *)
(* Wrong class name *)
@ -104,7 +114,7 @@ let disregards_whitespace_lines_and_comments =
in
assert_has_nullability_info storage
{class_name= "a.A"; method_name= Method "foo"; param_types= ["b.B"]}
~expected_nullability:{ret_nullability= Nonnull; param_nullability= [Nonnull]}
~expected_nullability:(Nonnull, [Nonnull])
~expected_file:"test.sig" ~expected_line:2 ;
(* Commented out signatures should be ignored *)
assert_no_info storage {class_name= "a.A"; method_name= Method "bar"; param_types= ["b.B"]}
@ -133,10 +143,10 @@ let overload_resolution =
(* a.b.SomeClass.foo with 1 param *)
assert_has_nullability_info storage
{class_name= "a.b.SomeClass"; method_name= Method "foo"; param_types= ["a.b.C1"]}
~expected_nullability:{ret_nullability= Nullable; param_nullability= [Nullable]} ;
~expected_nullability:(Nullable, [Nullable]) ;
assert_has_nullability_info storage
{class_name= "a.b.SomeClass"; method_name= Method "foo"; param_types= ["a.b.C2"]}
~expected_nullability:{ret_nullability= Nonnull; param_nullability= [Nullable]} ;
~expected_nullability:(Nonnull, [Nullable]) ;
(* wrong type *)
assert_no_info storage
{class_name= "a.b.SomeClass"; method_name= Method "foo"; param_types= ["a.b.C3"]} ;
@ -151,8 +161,7 @@ let overload_resolution =
{ class_name= "a.b.SomeClass"
; method_name= Method "foo"
; param_types= ["a.b.C1"; "a.b.C3"; "c.d.C4"] }
~expected_nullability:
{ret_nullability= Nullable; param_nullability= [Nullable; Nullable; Nonnull]} ;
~expected_nullability:(Nullable, [Nullable; Nullable; Nonnull]) ;
(* wrong param order *)
assert_no_info storage
{ class_name= "a.b.SomeClass"
@ -164,13 +173,13 @@ let overload_resolution =
(* possibility of constructor overload should be respected *)
assert_has_nullability_info storage
{class_name= "a.b.SomeClass"; method_name= Constructor; param_types= []}
~expected_nullability:{ret_nullability= Nonnull; param_nullability= []} ;
~expected_nullability:(Nonnull, []) ;
assert_has_nullability_info storage
{class_name= "a.b.SomeClass"; method_name= Constructor; param_types= ["a.b.C1"]}
~expected_nullability:{ret_nullability= Nonnull; param_nullability= [Nonnull]} ;
~expected_nullability:(Nonnull, [Nonnull]) ;
assert_has_nullability_info storage
{class_name= "a.b.SomeClass"; method_name= Constructor; param_types= ["a.b.C2"]}
~expected_nullability:{ret_nullability= Nonnull; param_nullability= [Nullable]} ;
~expected_nullability:(Nonnull, [Nullable]) ;
(* wrong param type *)
assert_no_info storage
{class_name= "a.b.SomeClass"; method_name= Constructor; param_types= ["a.b.C3"]}
@ -189,19 +198,19 @@ let can_add_several_files =
in
assert_has_nullability_info storage
{class_name= "a.A"; method_name= Method "foo"; param_types= ["b.B"]}
~expected_nullability:{ret_nullability= Nonnull; param_nullability= [Nonnull]}
~expected_nullability:(Nonnull, [Nonnull])
~expected_file:"file1.sig" ~expected_line:1 ;
(* 2. Add another file and check if we added info *)
let file2 = ["e.E#baz(f.F)"; "g.G#<init>(h.H, @Nullable i.I) @Nullable"] in
let storage = add_from_annot_file_and_check_success storage ~filename:"file2.sig" ~lines:file2 in
assert_has_nullability_info storage
{class_name= "e.E"; method_name= Method "baz"; param_types= ["f.F"]}
~expected_nullability:{ret_nullability= Nonnull; param_nullability= [Nonnull]}
~expected_nullability:(Nonnull, [Nonnull])
~expected_file:"file2.sig" ~expected_line:1 ;
(* 3. Ensure we did not forget the content from the first file *)
assert_has_nullability_info storage
{class_name= "a.A"; method_name= Method "foo"; param_types= ["b.B"]}
~expected_nullability:{ret_nullability= Nonnull; param_nullability= [Nonnull]}
~expected_nullability:(Nonnull, [Nonnull])
~expected_file:"file1.sig" ~expected_line:1

@ -14,12 +14,15 @@ let assert_parse_ok input expected_output =
let result = ThirdPartyMethod.parse input in
match result with
| Ok output ->
(* Check that it was parsed to the expected result*)
assert_equal output expected_output ~printer:(fun parse_result ->
Pp.string_of_pp pp_parse_result parse_result )
Pp.string_of_pp pp parse_result ) ;
(* Check also that the canonical representation matches the original *)
assert_equal (to_canonical_string output) input
| Error error ->
assert_failure
(F.asprintf "Expected '%s' to be parsed as %a, but got error %s instead" input
ThirdPartyMethod.pp_parse_result expected_output (string_of_parsing_error error))
(F.asprintf "Expected '%s' to be parsed, but got error %s instead" input
(string_of_parsing_error error))
let assert_parse_bad input =
@ -28,7 +31,7 @@ let assert_parse_bad input =
| Ok output ->
assert_failure
(F.asprintf "Expected '%s' to be NOT parsed, but was parsed as %a instead" input
ThirdPartyMethod.pp_parse_result output)
ThirdPartyMethod.pp output)
| Error _ ->
()
@ -38,38 +41,52 @@ let success_cases =
>:: fun _ ->
(* No params *)
assert_parse_ok "a.b.C#foo()"
( {class_name= "a.b.C"; method_name= Method "foo"; param_types= []}
, {ret_nullability= Nonnull; param_nullability= []} ) ;
{class_name= "a.b.C"; method_name= Method "foo"; params= []; ret_nullability= Nonnull} ;
assert_parse_ok "a.b.C#foo() @Nullable"
( {class_name= "a.b.C"; method_name= Method "foo"; param_types= []}
, {ret_nullability= Nullable; param_nullability= []} ) ;
{class_name= "a.b.C"; method_name= Method "foo"; params= []; ret_nullability= Nullable} ;
(* One param *)
assert_parse_ok "a.b.C#foo(c.d.E)"
( {class_name= "a.b.C"; method_name= Method "foo"; param_types= ["c.d.E"]}
, {ret_nullability= Nonnull; param_nullability= [Nonnull]} ) ;
{ class_name= "a.b.C"
; method_name= Method "foo"
; params= [("c.d.E", Nonnull)]
; ret_nullability= Nonnull } ;
assert_parse_ok "a.b.C#foo(@Nullable c.d.E)"
( {class_name= "a.b.C"; method_name= Method "foo"; param_types= ["c.d.E"]}
, {ret_nullability= Nonnull; param_nullability= [Nullable]} ) ;
{ class_name= "a.b.C"
; method_name= Method "foo"
; params= [("c.d.E", Nullable)]
; ret_nullability= Nonnull } ;
assert_parse_ok "a.b.C#foo(c.d.E) @Nullable"
( {class_name= "a.b.C"; method_name= Method "foo"; param_types= ["c.d.E"]}
, {ret_nullability= Nullable; param_nullability= [Nonnull]} ) ;
{ class_name= "a.b.C"
; method_name= Method "foo"
; params= [("c.d.E", Nonnull)]
; ret_nullability= Nullable } ;
assert_parse_ok "a.b.C#foo(@Nullable c.d.E) @Nullable"
( {class_name= "a.b.C"; method_name= Method "foo"; param_types= ["c.d.E"]}
, {ret_nullability= Nullable; param_nullability= [Nullable]} ) ;
{ class_name= "a.b.C"
; method_name= Method "foo"
; params= [("c.d.E", Nullable)]
; ret_nullability= Nullable } ;
(* Many params *)
assert_parse_ok "a.b.C#foo(c.d.E, a.b.C, x.y.Z)"
( {class_name= "a.b.C"; method_name= Method "foo"; param_types= ["c.d.E"; "a.b.C"; "x.y.Z"]}
, {ret_nullability= Nonnull; param_nullability= [Nonnull; Nonnull; Nonnull]} ) ;
{ class_name= "a.b.C"
; method_name= Method "foo"
; params= [("c.d.E", Nonnull); ("a.b.C", Nonnull); ("x.y.Z", Nonnull)]
; ret_nullability= Nonnull } ;
assert_parse_ok "a.b.C#foo(c.d.E, @Nullable a.b.C, x.y.Z)"
( {class_name= "a.b.C"; method_name= Method "foo"; param_types= ["c.d.E"; "a.b.C"; "x.y.Z"]}
, {ret_nullability= Nonnull; param_nullability= [Nonnull; Nullable; Nonnull]} ) ;
{ class_name= "a.b.C"
; method_name= Method "foo"
; params= [("c.d.E", Nonnull); ("a.b.C", Nullable); ("x.y.Z", Nonnull)]
; ret_nullability= Nonnull } ;
assert_parse_ok "a.b.C#foo(@Nullable c.d.E, a.b.C, @Nullable x.y.Z) @Nullable"
( {class_name= "a.b.C"; method_name= Method "foo"; param_types= ["c.d.E"; "a.b.C"; "x.y.Z"]}
, {ret_nullability= Nullable; param_nullability= [Nullable; Nonnull; Nullable]} ) ;
{ class_name= "a.b.C"
; method_name= Method "foo"
; params= [("c.d.E", Nullable); ("a.b.C", Nonnull); ("x.y.Z", Nullable)]
; ret_nullability= Nullable } ;
(* Constructor *)
assert_parse_ok "a.b.C#<init>(@Nullable c.d.E, a.b.C, x.y.Z) @Nullable"
( {class_name= "a.b.C"; method_name= Constructor; param_types= ["c.d.E"; "a.b.C"; "x.y.Z"]}
, {ret_nullability= Nullable; param_nullability= [Nullable; Nonnull; Nonnull]} )
{ class_name= "a.b.C"
; method_name= Constructor
; params= [("c.d.E", Nullable); ("a.b.C", Nonnull); ("x.y.Z", Nonnull)]
; ret_nullability= Nullable }
(* We intentionally don't test all bad cases.

Loading…
Cancel
Save