diff --git a/infer/src/nullsafe/ThirdPartyAnnotationInfo.ml b/infer/src/nullsafe/ThirdPartyAnnotationInfo.ml index 1a0f10097..be8263efb 100644 --- a/infer/src/nullsafe/ThirdPartyAnnotationInfo.ml +++ b/infer/src/nullsafe/ThirdPartyAnnotationInfo.ml @@ -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 ) diff --git a/infer/src/nullsafe/ThirdPartyAnnotationInfo.mli b/infer/src/nullsafe/ThirdPartyAnnotationInfo.mli index 28ce62e55..eb92f5ed1 100644 --- a/infer/src/nullsafe/ThirdPartyAnnotationInfo.mli +++ b/infer/src/nullsafe/ThirdPartyAnnotationInfo.mli @@ -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. *) diff --git a/infer/src/nullsafe/ThirdPartyMethod.ml b/infer/src/nullsafe/ThirdPartyMethod.ml index 4b03f2d79..ef8320dbd 100644 --- a/infer/src/nullsafe/ThirdPartyMethod.ml +++ b/infer/src/nullsafe/ThirdPartyMethod.ml @@ -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] " -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 -> "" | 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 diff --git a/infer/src/nullsafe/ThirdPartyMethod.mli b/infer/src/nullsafe/ThirdPartyMethod.mli index 3671f43ee..c3d5e8553 100644 --- a/infer/src/nullsafe/ThirdPartyMethod.mli +++ b/infer/src/nullsafe/ThirdPartyMethod.mli @@ -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] + #() *) diff --git a/infer/src/nullsafe/models.ml b/infer/src/nullsafe/models.ml index 981e61788..68d28528b 100644 --- a/infer/src/nullsafe/models.ml +++ b/infer/src/nullsafe/models.ml @@ -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) -> diff --git a/infer/src/nullsafe/unit/ThirdPartyAnnotationInfoTests.ml b/infer/src/nullsafe/unit/ThirdPartyAnnotationInfoTests.ml index 841488d67..1e5dae10f 100644 --- a/infer/src/nullsafe/unit/ThirdPartyAnnotationInfoTests.ml +++ b/infer/src/nullsafe/unit/ThirdPartyAnnotationInfoTests.ml @@ -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#(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 diff --git a/infer/src/nullsafe/unit/ThirdPartyMethodTests.ml b/infer/src/nullsafe/unit/ThirdPartyMethodTests.ml index f547e7664..dc58b4924 100644 --- a/infer/src/nullsafe/unit/ThirdPartyMethodTests.ml +++ b/infer/src/nullsafe/unit/ThirdPartyMethodTests.ml @@ -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#(@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.