Closed vladoiliev02 closed 1 year ago
Attempting to reduce to smaller test case shows some additional interesting incorrect behavior:
import java.util.List;
@interface NotBlank {
String message();
}
record Record1(
List<@NotBlank(message = "Not blank Record1 string") String> strings,
@NotBlank(message = "Not blank Record1 str") String str) {
}
public class X {
List<@NotBlank(message = "Not blank Record1 string") String> strings;
public static void main(String[] args) {
foo( new Record1(List.of(""), "Blah"));
}
static void foo(Record1 r) {}
}
This code compiled with eclipse master while javac correctly rejects it with:
X.java:8: error: annotation @NotBlank not applicable in this type context
List<@NotBlank(message = "Not blank Record1 string") String> strings,
^
X.java:13: error: annotation @NotBlank not applicable in this type context
List<@NotBlank(message = "Not blank Record1 string") String> strings;
^
2 errors
Hello @vladoiliev02 , can you provide a self contained complete example that shows the problem ? This is crucial - I don't want to fill in missing details (such as annotation targets etc)
Looks at first glance, ECJ went ahead and implemented some spec change that javac hasn't caught up with: see https://bugs.eclipse.org/bugs/show_bug.cgi?id=552082 and https://bugs.openjdk.org/browse/JDK-8231435 and in particular the comment https://bugs.eclipse.org/bugs/show_bug.cgi?id=552082#c6 which behavior difference I still see.
Given all this, it is best to work with a test case that shows the problem fully without my having to guess and fill in the blanks.
@stephan-herrmann and @jarthana - FYI.
That the code in https://bugs.eclipse.org/bugs/show_bug.cgi?id=552082#c6 still doesn't compile in javac and does with ecj worries me - I'll ask through my channels what exactly is going on there in JDK/javac
I worry that we have gone ahead and implemented something overeagerly in https://bugs.eclipse.org/bugs/show_bug.cgi?id=552082
JLS20 9.6.4.1 @Target ... still reads:
... If an annotation of type java.lang.annotation.Target is not present on the declaration of an annotation interface A, then A is applicable in all declaration contexts and in no type contexts.
Digging up a bit more, JLS and javac changed course via https://bugs.openjdk.org/browse/JDK-8261610 pulling back from https://bugs.openjdk.org/browse/JDK-8231435 with this text:
...
Per https://mail.openjdk.java.net/pipermail/compiler-dev/2021-February/016321.html the desired meaning of no-@Target is "all declaration contexts". This meaning includes:
- the type parameter declaration context added in Java SE 8 (and excluded from no-@Target by JSR 308, wrongly),
- the module declaration context added in Java SE 9, and
- the record component declaration context added in Java SE 16.
As further kinds of declaration are added to Java, it is intended that "all declaration contexts" will make annotations without an explicit @Target be applicable to those new kinds of declarations.
This meaning overrules JDK-8231435, which in 2019 expanded the meaning from:
"all declaration contexts that were present in Java SE 7, and no type contexts"
to:
"all declaration contexts and all type contexts".
The 2019 expansion was intended to put type annotations on an equal footing with declaration annotations, but was subsequently seen as a step too far given the disjoint roles typically played by type annotations and declaration annotations. A secondary concern arose in relation to the corner case where annotations in certain ambiguous locations are treated as both type annotations and declaration annotations -- this behavior is long standing and well specified, but bringing all the no-@Target annotations into its orbit was seen as undesirable from a JDK implementation/testing point of view.
Specifically, 9.6.4.1 should say: "If an annotation of type java.lang.annotation.Target is not present on the declaration of an annotation interface A, then A is applicable in all declaration contexts and in no type contexts." In effect, JSR 308 had the right idea to disallow type contexts, but went too far in trying to constrain the allowed declaration contexts.
...
I'll work on reversing course for ECJ - I think that has a strong influence on the current ticket.
@vladoiliev02 - to be 100% sure, I will need a standalone test case that compiles and runs (with failure) - TIA. You are in a better position to stub out portions of code to come up with a minimal test case than I am
Well, JLS certainly reversed course - but looks like javac never implemented https://bugs.openjdk.org/browse/JDK-8231435 in the first place ? All of javac 14-19 reject this code:
@interface Simple {}
class Test { <@Simple T> void m(T arg1) {} }
while we compile this post https://bugs.eclipse.org/bugs/show_bug.cgi?id=552082
Well, JLS certainly reversed course - but looks like javac never implemented https://bugs.openjdk.org/browse/JDK-8231435 in the first place ? All of javac 14-19 reject this code:
@interface Simple {} class Test { <@Simple T> void m(T arg1) {} }
while we compile this post https://bugs.eclipse.org/bugs/show_bug.cgi?id=552082
Javac has fixed this for JDK21 here: https://bugs.openjdk.org/browse/JDK-8304169
I have come up with a self contained example, although I am not sure why the exception is not thrown, but looking at the decompiled .class file, the error is still persisting. Here is the code:
@Target({ElementType.TYPE_USE})
@interface Annotation {
}
record Record(
@Annotation
List<@Annotation String> strings
) {
}
public class Main {
public static void main(String[] args) throws Exception {
Record record = new Record(List.of(""));
}
}
And here is a picture of the decompiled file:
The annotation is still being repeated which will eventually cause the same problem from the above example with Spring boot.
Here it also does not matter if the annotation on the list and inside on the parameter are the same or are different annotations. The result is the same - the one inside on the generic parameter gets repeated.
" If an annotation of type java.lang.annotation.Target is not present on the declaration of an annotation interface A, then A is applicable in all declaration contexts and in no type contexts. " - from your previous reply. I do not think that this is causing the issue since the original @NotBlank annotation from the javax validation API has a @Target.
Looking at the first exception I have sent the problem occurs when the validation library is iterating over the annotations on the different fields, it then sees the duplicate annotation we see in the picture above and throws the above provided exception. I don't really understand how to get the annotations and iterate over them, but if you are able to do that you will definitely see that the annotations in the generic parameter have doubled.
Thanks, I will follow up - yes with the shortened test case, it looks likely an orthogonal issue.
I was able to come up with a test case that fails:
@Target(ElementType.TYPE_USE)
@Retention(RetentionPolicy.RUNTIME)
@interface Ann {
}
record Record(
@Ann
List<@Ann String> list
) {
}
class DuplicateAnnotationTest {
@Test
void testNotDuplicatingAnnotationDoesNotThrow() throws Exception {
AnnotatedParameterizedType listMethodReturnType = (AnnotatedParameterizedType)
Record.class.getDeclaredMethod("list").getAnnotatedReturnType();
assertDoesNotThrow(listMethodReturnType::getAnnotatedActualTypeArguments,
"Should not throw AnnotationFormatError.");
}
}
This test case fails because of the duplicate annotation we see in the decompiled .class file. If it was a Map.class
not a List.class
, the annotations on both of the map's parameters are duplicated.
No, that test case doesn't demonstrate a crash due to duplicate annotation, It refuses to compile :-) This one does:
import java.lang.annotation.*;
import java.lang.annotation.Target;
import java.util.List;
import java.lang.reflect.AnnotatedParameterizedType;
@Target(ElementType.TYPE_USE)
@Retention(RetentionPolicy.RUNTIME)
@interface Ann {
}
record Record(
@Ann
List<@Ann String> list
) {
}
public class X {
static void assertDoesNotThrow(Runnable exe, String message) {
exe.run();
}
public static void main(String [] args) throws Exception {
AnnotatedParameterizedType listField = (AnnotatedParameterizedType) Record.class.getDeclaredMethod("list").getAnnotatedReturnType();
assertDoesNotThrow(listField::getAnnotatedActualTypeArguments, "Should not throw duplicate annotation exception.");
}
}
Thanks a lot!
Looks like a bad side effect of https://bugs.eclipse.org/bugs/show_bug.cgi?id=571905
Fix under consideration : https://github.com/eclipse-jdt/eclipse.jdt.core/pull/1101
When will I be able to see the changes in my IDE?
I expect to integrate the fix early next week - for 4.29 M1.
I expect to integrate the fix early next week - for 4.29 M1.
Wait, does this mean it will be GA in September?
Wait, does this mean it will be GA in September?
You could use nightly SDK build if you like. I do it every day since years, and in most cases it just works.
The following code compiles successfully using the eclipse compiler and throws a
java.lang.annotation.AnnotationFormatError: Duplicate annotation...
exception on runtime. The same code compiled with a different compiler has no issues. This leads me to believe that this is an issue with the eclipse implementation of the java compiler. Using a decompiler on the '.class' file shows that really the@NotBlank
annotation has been repeated inside the type parameter of theList
post compilation.The problem occurs when calling the patch endpoint in the controller above. The exception occurs during validation of the dto.
Exception: