diff --git a/infer/src/IR/JavaClassName.ml b/infer/src/IR/JavaClassName.ml index bfa6e3c47..44a4cd9cd 100644 --- a/infer/src/IR/JavaClassName.ml +++ b/infer/src/IR/JavaClassName.ml @@ -72,14 +72,29 @@ let strip_anonymous_suffixes_if_present classname = strip_recursively classname 0 +(* Strips everything after $Lambda$ (if it is a lambda-class), + and returns the result string together with if it was stripped *) +let strip_lambda_if_present classname = + match String.substr_index classname ~pattern:"$Lambda$" with + | Some index -> + (String.prefix classname index, true) + | None -> + (classname, false) + + (* - Anonymous classes have suffixes in form of $; but they can be nested inside of each other. + Anonymous classes have two forms: + - classic anonymous classes: suffixes in form of $. + - classes corresponding to lambda-expressions: they are manifested as $Lambda$. + - two forms above nested inside 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 *) + In general case anonymous class name looks something like + Class$NestedClass$1$17$5$Lambda$_1_2, 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 without_lambda, was_lambda_stripped = strip_lambda_if_present classname in + let outer_class_name, nesting_level = strip_anonymous_suffixes_if_present without_lambda in + let was_stripped = was_lambda_stripped || nesting_level > 0 in + if was_stripped 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 diff --git a/infer/src/IR/JavaClassName.mli b/infer/src/IR/JavaClassName.mli index 06ea98f88..de3147946 100644 --- a/infer/src/IR/JavaClassName.mli +++ b/infer/src/IR/JavaClassName.mli @@ -30,15 +30,15 @@ val is_external_via_config : t -> bool (** Considered external based on config flags. *) 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 *) +(** True if it is either "classic" anonymous Java class: + https://docs.oracle.com/javase/tutorial/java/javaOO/anonymousclasses.html, or a synthetic Java + class corresponding to a lambda expression. *) 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 +(** 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. + In general case, BOTH anonymous classes and user-defined classes can be nested: + SomeClass$NestedClass$1$17$5. In this example, we should return SomeClass$NestedClass. If this is not an anonymous class, returns [None]. *) diff --git a/infer/src/unit/JavaClassNameTests.ml b/infer/src/unit/JavaClassNameTests.ml index 86023c6d2..5e3aeee2c 100644 --- a/infer/src/unit/JavaClassNameTests.ml +++ b/infer/src/unit/JavaClassNameTests.ml @@ -46,7 +46,7 @@ let test_from_string = let test_anonymous = - "test_from_string" + "test_anonymous" >:: fun _ -> (* If it is not an anonymous class, we expect this to return None *) get_user_defined_class_if_anonymous_inner @@ -80,6 +80,23 @@ let test_anonymous = |> assert_some |> assert_equal_to ~expected_package:(Some "some.package") ~expected_classname:"SomeClass$NestedClass$AgainNestedClass" ; + (* If it is a lambda class, we expect this to be detected *) + get_user_defined_class_if_anonymous_inner + (make ~package:(Some "some.package") ~classname:"SomeClass$Lambda$_4_1") + |> assert_some + |> assert_equal_to ~expected_package:(Some "some.package") ~expected_classname:"SomeClass" ; + (* Lambda might be inside anonymous (or several ones in general case) *) + get_user_defined_class_if_anonymous_inner + (make ~package:(Some "some.package") ~classname:"SomeClass$1$7$Lambda$_4_1") + |> assert_some + |> assert_equal_to ~expected_package:(Some "some.package") ~expected_classname:"SomeClass" ; + (* The most general case: nested class, lambda, and anonymous mixed *) + get_user_defined_class_if_anonymous_inner + (make ~package:(Some "some.package") + ~classname:"SomeClass$NestedClass$7$1$Lambda$_4_1$2$Lambda$_7_18$19$16") + |> assert_some + |> assert_equal_to ~expected_package:(Some "some.package") + ~expected_classname:"SomeClass$NestedClass" ; (* 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") diff --git a/infer/tests/codetoanalyze/java/nullsafe-default/issues.exp b/infer/tests/codetoanalyze/java/nullsafe-default/issues.exp index 3df51b225..f8c2bc7a6 100644 --- a/infer/tests/codetoanalyze/java/nullsafe-default/issues.exp +++ b/infer/tests/codetoanalyze/java/nullsafe-default/issues.exp @@ -328,7 +328,6 @@ codetoanalyze/java/nullsafe-default/PropagatesNullable.java, codetoanalyze.java. codetoanalyze/java/nullsafe-default/PropagatesNullable.java, codetoanalyze.java.nullsafe_default.TestPropagatesNullable$TestSecondParameter.test(java.lang.String,java.lang.String):void, 7, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`nullable(...)` is nullable and is not locally checked for null when calling `length()`.] codetoanalyze/java/nullsafe-default/PropagatesNullable.java, codetoanalyze.java.nullsafe_default.TestPropagatesNullable$TestSecondParameter.test(java.lang.String,java.lang.String):void, 11, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`nullable(...)` is nullable and is not locally checked for null when calling `length()`.] codetoanalyze/java/nullsafe-default/PropagatesNullable.java, codetoanalyze.java.nullsafe_default.TestPropagatesNullable$TestSecondParameter.test(java.lang.String,java.lang.String):void, 15, ERADICATE_NULLABLE_DEREFERENCE, no_bucket, WARNING, [`nullable(...)` is nullable and is not locally checked for null when calling `length()`.] -codetoanalyze/java/nullsafe-default/ReturnNotNullable.java, Linters_dummy_method, 1, ERADICATE_META_CLASS_CAN_BE_NULLSAFE, no_bucket, ADVICE, [Congrats! Class codetoanalyze.java.nullsafe_default.ReturnNotNullable$Lambda$_9_1 is free of nullability issues. Mark it `@Nullsafe(Nullsafe.Mode.Local)` to prevent regressions.], ReturnNotNullable$Lambda$_9_1, codetoanalyze.java.nullsafe_default, issues: 0, curr_mode: "Default", promote_mode: "LocalTrustAll" codetoanalyze/java/nullsafe-default/ReturnNotNullable.java, Linters_dummy_method, 19, ERADICATE_META_CLASS_NEEDS_IMPROVEMENT, no_bucket, INFO, [], ReturnNotNullable, codetoanalyze.java.nullsafe_default, issues: 9, curr_mode: "Default" codetoanalyze/java/nullsafe-default/ReturnNotNullable.java, Linters_dummy_method, 154, ERADICATE_META_CLASS_CAN_BE_NULLSAFE, no_bucket, ADVICE, [Congrats! Class codetoanalyze.java.nullsafe_default.ReturnNotNullable$E is free of nullability issues. Mark it `@Nullsafe(value = Nullsafe.Mode.LOCAL, trustOnly = @Nullsafe.TrustList({}))` to prevent regressions.], ReturnNotNullable$E, codetoanalyze.java.nullsafe_default, issues: 0, curr_mode: "Default", promote_mode: "Strict" codetoanalyze/java/nullsafe-default/ReturnNotNullable.java, Linters_dummy_method, 191, ERADICATE_META_CLASS_NEEDS_IMPROVEMENT, no_bucket, INFO, [], ReturnNotNullable$ConditionalAssignment, codetoanalyze.java.nullsafe_default, issues: 1, curr_mode: "Default"