diff --git a/infer/src/IR/JavaClassName.ml b/infer/src/IR/JavaClassName.ml index 84824cab8..7de67093b 100644 --- a/infer/src/IR/JavaClassName.ml +++ b/infer/src/IR/JavaClassName.ml @@ -10,7 +10,11 @@ module F = Format module L = Logging (** invariant: if [package = Some str] then [not (String.equal str "")] *) -type t = {classname: string; package: string option} [@@deriving compare] +type t = {classname: string; package: string option} [@@deriving compare, equal] + +let make ~package ~classname = + match package with Some "" -> {package= None; classname} | _ -> {package; classname} + let from_string str = match String.rsplit2 str ~on:'.' with @@ -40,19 +44,39 @@ let package {package} = package let classname {classname} = classname -let is_anonymous_inner_class_name {classname} = - match String.rsplit2 classname ~on:'$' with - | Some (_, s) -> - let is_int = - try - ignore (int_of_string (String.strip s)) ; - true - with Failure _ -> false - in - is_int - | None -> - false +let is_int s = + try + ignore (int_of_string s) ; + true + with Failure _ -> false + + +(* Strips $ suffixes from the class name, and return how many were stripped *) +let strip_anonymous_suffixes_if_present classname = + let rec strip_recursively classname nesting_level = + match String.rsplit2 classname ~on:'$' with + | Some (outer, suffix) when is_int (String.strip suffix) -> + (* Suffix is an integer - that was an anonymous class. + But it could be nested inside another anonymous class as well *) + strip_recursively outer (nesting_level + 1) + | _ -> + (* Suffix is not an integer or not present - not an anonymous class *) + (classname, nesting_level) + in + strip_recursively classname 0 + + +(* + Anonymous classes have suffixes in form of $; but they can be nested inside of each other. + Also non-anonymous (user-defined) name can be nested as well (Class$NestedClass). + So in general case anonymous class name looks something like + Class$NestedClass$1$17$5, and we need to return Class$NestedClass *) +let get_user_defined_class_if_anonymous_inner {package; classname} = + let outer_class_name, nesting_level = strip_anonymous_suffixes_if_present classname in + if nesting_level > 0 then Some {package; classname= outer_class_name} else None + +let is_anonymous_inner_class_name t = get_user_defined_class_if_anonymous_inner t |> is_some let is_external_via_config t = let package = package t in diff --git a/infer/src/IR/JavaClassName.mli b/infer/src/IR/JavaClassName.mli index 0d42a35e4..835bb410f 100644 --- a/infer/src/IR/JavaClassName.mli +++ b/infer/src/IR/JavaClassName.mli @@ -7,7 +7,9 @@ open! IStd -type t [@@deriving compare] +type t [@@deriving compare, equal] + +val make : package:string option -> classname:string -> t val from_string : string -> t @@ -28,3 +30,13 @@ val is_external_via_config : t -> bool val is_anonymous_inner_class_name : t -> bool (** True if it is anonymous Java class: https://docs.oracle.com/javase/tutorial/java/javaOO/anonymousclasses.html *) + +val get_user_defined_class_if_anonymous_inner : t -> t option +(** If the current class is anonymous ([is_anonymous_inner_class_name] is true), Return the + corresponding user defined (not anonymous) class this anonymous class belongs to. + + In general case, BOTH anonymous classes and user-defined classes can be nested, so the most + general example looks like So in general case anonymous class name looks something like + Class$NestedClass$1$17$5. This function should return Class$NestedClass for this case. + + If this is not an anonymous class, returns [None]. *) diff --git a/infer/src/unit/JavaClassNameTests.ml b/infer/src/unit/JavaClassNameTests.ml new file mode 100644 index 000000000..1c0e67fe8 --- /dev/null +++ b/infer/src/unit/JavaClassNameTests.ml @@ -0,0 +1,92 @@ +(* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + *) + +open! IStd +open OUnit2 +open JavaClassName +module F = Format + +let assert_equal_to classname ~expected_package ~expected_classname = + assert_equal ~printer:JavaClassName.to_string classname + (make ~package:expected_package ~classname:expected_classname) + + +let assert_some = function Some a -> a | None -> assert_failure "Expected Some, got None" + +let assert_none = function Some _ -> assert_failure "Expected None, got Some" | None -> () + +let test_from_string = + "test_from_string" + >:: fun _ -> + assert_equal_to + (from_string "some.package.SomeClass") + ~expected_package:(Some "some.package") ~expected_classname:"SomeClass" ; + assert_equal_to (from_string "SomeClass") ~expected_package:None ~expected_classname:"SomeClass" ; + assert_equal_to + (from_string "some.package.SomeClass$NestedClass") + ~expected_package:(Some "some.package") ~expected_classname:"SomeClass$NestedClass" ; + assert_equal_to + (from_string "SomeClass$NestedClass") + ~expected_package:None ~expected_classname:"SomeClass$NestedClass" ; + (* anonymous classes *) + assert_equal_to + (from_string "some.package.SomeClass$1") + ~expected_package:(Some "some.package") ~expected_classname:"SomeClass$1" ; + (* anonymous classes can be of nested levels *) + assert_equal_to + (from_string "some.package.SomeClass$1$3") + ~expected_package:(Some "some.package") ~expected_classname:"SomeClass$1$3" ; + (* anonymous classes can be inside nested *) + assert_equal_to + (from_string "SomeClass$NestedClass$1$3") + ~expected_package:None ~expected_classname:"SomeClass$NestedClass$1$3" + + +let test_anonymous = + "test_from_string" + >:: fun _ -> + (* If it is not an anonymous class, we expect this to return None *) + get_user_defined_class_if_anonymous_inner + (make ~package:(Some "some.package") ~classname:"SomeClass") + |> assert_none ; + get_user_defined_class_if_anonymous_inner (make ~package:None ~classname:"SomeClass") + |> assert_none ; + get_user_defined_class_if_anonymous_inner + (make ~package:(Some "some.package") ~classname:"SomeClass$SomeNestedClass$AgainNestedClass") + |> assert_none ; + (* If it is an anonymous class, we expect this to be detected *) + get_user_defined_class_if_anonymous_inner + (make ~package:(Some "some.package") ~classname:"SomeClass$17") + |> assert_some + |> assert_equal_to ~expected_package:(Some "some.package") ~expected_classname:"SomeClass" ; + (* Can be several nested anonymous classes *) + get_user_defined_class_if_anonymous_inner + (make ~package:(Some "some.package") ~classname:"SomeClass$17$23$1") + |> assert_some + |> assert_equal_to ~expected_package:(Some "some.package") ~expected_classname:"SomeClass" ; + (* Can be nested class with anonymous class *) + get_user_defined_class_if_anonymous_inner + (make ~package:(Some "some.package") ~classname:"SomeClass$NestedClass$AgainNestedClass$17") + |> assert_some + |> assert_equal_to ~expected_package:(Some "some.package") + ~expected_classname:"SomeClass$NestedClass$AgainNestedClass" ; + (* Can be nested class AND several nested anonymous classes *) + get_user_defined_class_if_anonymous_inner + (make ~package:(Some "some.package") + ~classname:"SomeClass$NestedClass$AgainNestedClass$17$23$1") + |> assert_some + |> assert_equal_to ~expected_package:(Some "some.package") + ~expected_classname:"SomeClass$NestedClass$AgainNestedClass" ; + (* If package was empty, everything should still work *) + get_user_defined_class_if_anonymous_inner + (make ~package:None ~classname:"SomeClass$NestedClass$AgainNestedClass$17$23$1") + |> assert_some + |> assert_equal_to ~expected_package:None + ~expected_classname:"SomeClass$NestedClass$AgainNestedClass" + + +let tests = "JavaClassNameTests" >::: [test_from_string; test_anonymous] diff --git a/infer/src/unit/inferunit.ml b/infer/src/unit/inferunit.ml index 6588334a7..7f9389194 100644 --- a/infer/src/unit/inferunit.ml +++ b/infer/src/unit/inferunit.ml @@ -35,6 +35,7 @@ let () = ; DifferentialTests.tests ; FileDiffTests.tests ; IListTests.tests + ; JavaClassNameTests.tests ; JavaProfilerSamplesTest.tests ; LivenessTests.tests ; LRUHashtblTests.tests