From 59d2b4b9a4a77138d47c6fab314db6d8e15c7eaf Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 22 Oct 2023 13:01:15 -0700 Subject: [PATCH 1/6] Don't write irrelevant annotations in .ajava files --- docs/manual/inference.tex | 2 +- ...holeProgramInferenceJavaParserStorage.java | 75 ++++++++++++- .../type/GenericAnnotatedTypeFactory.java | 106 ++++++++++++++++-- .../framework/type/QualifierHierarchy.java | 4 +- .../javacutil/TypesUtils.java | 8 +- 5 files changed, 170 insertions(+), 25 deletions(-) diff --git a/docs/manual/inference.tex b/docs/manual/inference.tex index f44283650ee..0242fd84e6b 100644 --- a/docs/manual/inference.tex +++ b/docs/manual/inference.tex @@ -135,7 +135,7 @@ A typical invocation of \ is \begin{Verbatim} - wpi.sh -- --checker nullness + $CHECKERFRAMEWORK/bin/wpi.sh -- --checker nullness \end{Verbatim} The result is a set of log files placed in the \ folder of the diff --git a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java index 53c4a8df117..be7faeb78a7 100644 --- a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java +++ b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java @@ -23,9 +23,15 @@ import com.github.javaparser.ast.expr.NormalAnnotationExpr; import com.github.javaparser.ast.expr.ObjectCreationExpr; import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr; +import com.github.javaparser.ast.type.ArrayType; import com.github.javaparser.ast.type.ClassOrInterfaceType; +import com.github.javaparser.ast.type.IntersectionType; +import com.github.javaparser.ast.type.PrimitiveType; import com.github.javaparser.ast.type.Type; import com.github.javaparser.ast.type.TypeParameter; +import com.github.javaparser.ast.type.UnionType; +import com.github.javaparser.ast.type.VarType; +import com.github.javaparser.ast.type.WildcardType; import com.github.javaparser.ast.visitor.CloneVisitor; import com.github.javaparser.ast.visitor.VoidVisitor; import com.github.javaparser.printer.DefaultPrettyPrinter; @@ -1062,6 +1068,53 @@ public void writeResultsToFile(OutputFormat outputFormat, BaseTypeChecker checke modifiedFiles.clear(); } + /** + * Returns true if the annotation is relevant (where it appears in the program). This + * implementation is conservative and only returns false if the qualifier is definitely not + * relevant. + * + * @param anno an annotation + * @return true if the annotation might be relevant + */ + boolean annotationIsRelevant(AnnotationExpr anno) { + // This is a fully-qualified name. + String aName = anno.getNameAsString(); + if (!atypeFactory.isSupportedQualifier(aName)) { + // The annotation might be a declaration qualifier, such as a side effect specification. + return true; + } + Node parentNode = anno.getParentNode().get(); + + if (parentNode instanceof ArrayType) { + return ((GenericAnnotatedTypeFactory) atypeFactory).arraysAreRelevant(); + } + if (parentNode instanceof ClassOrInterfaceType) { + ClassOrInterfaceType classType = (ClassOrInterfaceType) parentNode; + String simpleName = classType.getName().toString(); + String scopedName = classType.getNameWithScope(); + // TODO: Do I need to remove type parameters? + return ((GenericAnnotatedTypeFactory) atypeFactory).isRelevant(simpleName) + || ((GenericAnnotatedTypeFactory) atypeFactory).isRelevant(scopedName); + } + if (parentNode instanceof IntersectionType) { + return true; // TODO + } + if (parentNode instanceof PrimitiveType) { + return ((GenericAnnotatedTypeFactory) atypeFactory).isRelevant(parentNode.toString()); + } + if (parentNode instanceof UnionType) { + return true; // TODO + } + if (parentNode instanceof VarType) { + return ((GenericAnnotatedTypeFactory) atypeFactory).nonprimitivesAreRelevant(); + } + if (parentNode instanceof WildcardType) { + return true; // TODO + } + + throw new Error("What parent? " + parentNode.getClass().getSimpleName() + " " + parentNode); + } + /** * Write an ajava file to disk. * @@ -1071,14 +1124,15 @@ public void writeResultsToFile(OutputFormat outputFormat, BaseTypeChecker checke private void writeAjavaFile(File outputPath, CompilationUnitAnnos root) { try (Writer writer = new BufferedWriter(new FileWriter(outputPath))) { - // JavaParser can output using lexical preserving printing, which writes the file such - // that its formatting is close to the original source file it was parsed from as - // possible. Currently, this feature is very buggy and crashes when adding annotations - // in certain locations. This implementation could be used instead if it's fixed in - // JavaParser. + // This implementation uses JavaParser's lexical preserving printing, which writes the file + // such that its formatting is close to the original source file it was parsed from as + // possible. It is commented out because, this feature is very buggy and crashes when adding + // annotations in certain locations. // LexicalPreservingPrinter.print(root.declaration, writer); - // Do not print invisible qualifiers, to avoid cluttering the output. + // To avoid cluttering the output, do not print: + // * invisible qualifiers + // * irrelevant qualifiers. Set invisibleQualifierNames = getInvisibleQualifierNames(this.atypeFactory); DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter() { @@ -1091,6 +1145,9 @@ public void visit(MarkerAnnotationExpr n, Void arg) { if (invisibleQualifierNames.contains(n.getName().toString())) { return; } + if (!annotationIsRelevant(n)) { + return; + } super.visit(n, arg); } @@ -1099,6 +1156,9 @@ public void visit(SingleMemberAnnotationExpr n, Void arg) { if (invisibleQualifierNames.contains(n.getName().toString())) { return; } + if (!annotationIsRelevant(n)) { + return; + } super.visit(n, arg); } @@ -1107,6 +1167,9 @@ public void visit(NormalAnnotationExpr n, Void arg) { if (invisibleQualifierNames.contains(n.getName().toString())) { return; } + if (!annotationIsRelevant(n)) { + return; + } super.visit(n, arg); } }; diff --git a/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java b/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java index 599e50fc542..0e57e7a2038 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java +++ b/framework/src/main/java/org/checkerframework/framework/type/GenericAnnotatedTypeFactory.java @@ -187,15 +187,25 @@ public abstract class GenericAnnotatedTypeFactory< *

Although a {@code Class} object exists for every element, this does not contain those * {@code Class} objects because the elements will be compared to TypeMirrors for which Class * objects may not exist (they might not be on the classpath). + * + *

For their names, see {@link #relevantJavaTypeNames}. */ public final @Nullable Set relevantJavaTypes; /** - * Whether users may write type annotations on arrays. Ignored unless {@link #relevantJavaTypes} - * is non-null. + * The fully-qualified names and simple names of the types in {@link #relevantJavaTypes}. */ + public final @Nullable Set relevantJavaTypeNames; + + /** Whether users may write type annotations on arrays. */ protected final boolean arraysAreRelevant; + /** + * Whether users may write type annotations on non-primitives (classes, arrays, etc.). This is + * redundant with the value of {@link #relevantJavaTypes} but is included for efficiency. + */ + protected final boolean nonprimitivesAreRelevant; + // Flow related fields /** Should flow be used by default? */ @@ -358,26 +368,49 @@ protected GenericAnnotatedTypeFactory(BaseTypeChecker checker, boolean useFlow) checker.getClass().getAnnotation(RelevantJavaTypes.class); if (relevantJavaTypesAnno == null) { this.relevantJavaTypes = null; + this.relevantJavaTypeNames = null; this.arraysAreRelevant = true; + this.nonprimitivesAreRelevant = true; } else { Types types = getChecker().getTypeUtils(); Elements elements = getElementUtils(); Class[] classes = relevantJavaTypesAnno.value(); - this.relevantJavaTypes = new HashSet<>(CollectionsPlume.mapCapacity(classes.length)); + Set relevantJavaTypesTemp = + new HashSet<>(CollectionsPlume.mapCapacity(classes.length)); + Set relevantJavaTypeNamesTemp = + new HashSet<>(CollectionsPlume.mapCapacity(classes.length)); boolean arraysAreRelevantTemp = false; + boolean nonprimitivesAreRelevantTemp = false; for (Class clazz : classes) { if (clazz == Object[].class) { arraysAreRelevantTemp = true; + nonprimitivesAreRelevantTemp = true; } else if (clazz.isArray()) { throw new TypeSystemError( "Don't use arrays other than Object[] in @RelevantJavaTypes on " + this.getClass().getSimpleName()); } else { TypeMirror relevantType = TypesUtils.typeFromClass(clazz, types, elements); - relevantJavaTypes.add(types.erasure(relevantType)); + TypeMirror erased = types.erasure(relevantType); + relevantJavaTypesTemp.add(erased); + String typeString = erased.toString(); + relevantJavaTypeNamesTemp.add(typeString); + if (clazz.isPrimitive()) { + nonprimitivesAreRelevantTemp = true; + } else { + int dotIndex = typeString.lastIndexOf('.'); + if (dotIndex != -1) { + // It's a fully-qualified name. Add the simple name as well. + // TODO: This might not handle a user writing a nested class like "Map.Entry". + relevantJavaTypeNamesTemp.add(typeString.substring(dotIndex + 1)); + } + } } } + this.relevantJavaTypes = Collections.unmodifiableSet(relevantJavaTypesTemp); + this.relevantJavaTypeNames = Collections.unmodifiableSet(relevantJavaTypeNamesTemp); this.arraysAreRelevant = arraysAreRelevantTemp; + this.nonprimitivesAreRelevant = nonprimitivesAreRelevantTemp; } contractsUtils = createContractsFromMethod(); @@ -2393,8 +2426,9 @@ private static void log(String format, Object... args) { * Returns true if users can write type annotations from this type system directly on the given * Java type. * - *

May return false for a compound type (for which it is possible to write type qualifiers on - * elements of the type). + *

For a compound type, returns true only if a programmer may write a type qualifier on the top + * level of the compound type. That is, this method may return false, when it is possible to write + * type qualifiers on elements of the type. * *

Subclasses should override {@code #isRelevantImpl} instead of this method. * @@ -2403,12 +2437,15 @@ private static void log(String format, Object... args) { * Java type */ public final boolean isRelevant(TypeMirror tm) { + if (relevantJavaTypes == null) { + return true; + } if (tm.getKind() != TypeKind.PACKAGE && tm.getKind() != TypeKind.MODULE) { tm = types.erasure(tm); } - Boolean cachedResult = isRelevantCache.get(tm); - if (cachedResult != null) { - return cachedResult; + Boolean resultBoxed = isRelevantCache.get(tm); + if (resultBoxed != null) { + return resultBoxed; } boolean result = isRelevantImpl(tm); isRelevantCache.put(tm, result); @@ -2419,8 +2456,9 @@ public final boolean isRelevant(TypeMirror tm) { * Returns true if users can write type annotations from this type system directly on the given * Java type. * - *

May return false for a compound type (for which it is possible to write type qualifiers on - * elements of the type). + *

For a compound type, returns true only if it a programmer may write a type qualifier on the + * top level of the compound type. That is, this method may return false, when it is possible to + * write type qualifiers on elements of the type. * *

Subclasses should override {@code #isRelevantImpl} instead of this method. * @@ -2439,12 +2477,19 @@ public final boolean isRelevant(AnnotatedTypeMirror tm) { *

Clients should never call this. Call {@link #isRelevant} instead. This is a helper method * for {@link #isRelevant}. * + *

This should not be called if {@code relevantJavaTypes == null || + * relevantJavaTypes.contains(tm))}. + * * @param tm a type * @return true if users can write type annotations from this type system on the given Java type */ protected boolean isRelevantImpl(TypeMirror tm) { - if (relevantJavaTypes == null || relevantJavaTypes.contains(tm)) { + if (relevantJavaTypes == null) { + return true; + } + + if (relevantJavaTypes.contains(tm)) { return true; } @@ -2524,6 +2569,43 @@ protected boolean isRelevantImpl(TypeMirror tm) { } } + /** + * Returns true if users can write type annotations from this type system directly on the given + * Java type. + * + *

For a compound type, returns true only if it is permitted to write a type qualifier on the + * top level of the compound type. That is, this method may return false, when it is possible to + * write type qualifiers on elements of the type. + * + *

Subclasses should override {@code #isRelevantImpl} instead of this method. + * + * @param type a fully-qualified or simple type; should not be an array (use {@link + * #arraysAreRelevant} instead) + * @return true if users can write type annotations from this type system directly on the given + * Java type + */ + public final boolean isRelevant(String type) { + return relevantJavaTypeNames == null || relevantJavaTypeNames.contains(type); + } + + /** + * Returns true if users can write type annotations from this type system on array types. + * + * @return true if users can write type annotations from this type system on array types + */ + public final boolean arraysAreRelevant() { + return arraysAreRelevant; + } + + /** + * Returns true if users can write type annotations from this type system on non-primitive types. + * + * @return true if users can write type annotations from this type system on non-primitive types + */ + public final boolean nonprimitivesAreRelevant() { + return nonprimitivesAreRelevant; + } + /** The cached message about relevant types. */ private @MonotonicNonNull String irrelevantExtraMessage = null; diff --git a/framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java b/framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java index 523148fe1b6..cf985fd4509 100644 --- a/framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java +++ b/framework/src/main/java/org/checkerframework/framework/type/QualifierHierarchy.java @@ -43,9 +43,9 @@ public QualifierHierarchy(GenericAnnotatedTypeFactory atypeFactory) } /** - * Determine whether this is valid. + * Determine whether this QualifierHierarchy is valid. * - * @return true if this is valid + * @return true if this QualifierHierarchy is valid */ public boolean isValid() { return true; diff --git a/javacutil/src/main/java/org/checkerframework/javacutil/TypesUtils.java b/javacutil/src/main/java/org/checkerframework/javacutil/TypesUtils.java index dddb3cb1e5c..61fef95e135 100644 --- a/javacutil/src/main/java/org/checkerframework/javacutil/TypesUtils.java +++ b/javacutil/src/main/java/org/checkerframework/javacutil/TypesUtils.java @@ -171,10 +171,10 @@ public static Class getClassFromType(TypeMirror typeMirror) { } /** - * Returns the simple type name, without annotations. + * Returns the simple type name, without annotations but including array brackets. * * @param type a type - * @return the simple type name, without annotations + * @return the simple type name */ public static String simpleTypeName(TypeMirror type) { switch (type.getKind()) { @@ -218,10 +218,10 @@ public static String simpleTypeName(TypeMirror type) { } /** - * Returns the binary name. + * Returns the binary name of a type. * * @param type a type - * @return the binary name + * @return its binary name */ public static @BinaryName String binaryName(TypeMirror type) { if (type.getKind() != TypeKind.DECLARED) { From 5e51863bd6204471b527255652dd3f1032648963 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 22 Oct 2023 16:18:11 -0700 Subject: [PATCH 2/6] Remove nonsensical annotations --- .../DependentTypesViewpointAdaptationTest.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/checker/tests/ainfer-index/non-annotated/DependentTypesViewpointAdaptationTest.java b/checker/tests/ainfer-index/non-annotated/DependentTypesViewpointAdaptationTest.java index 93d925e3b4c..9ce10b617ad 100644 --- a/checker/tests/ainfer-index/non-annotated/DependentTypesViewpointAdaptationTest.java +++ b/checker/tests/ainfer-index/non-annotated/DependentTypesViewpointAdaptationTest.java @@ -59,13 +59,11 @@ public static void testThisInference( public void compute5( DependentTypesViewpointAdaptationTest this, DependentTypesViewpointAdaptationTest other) { - // :: warning: (assignment) - @SameLen("this") DependentTypesViewpointAdaptationTest myOther = other; + DependentTypesViewpointAdaptationTest myOther = other; } // Same as compute5, but without an explicit this parameter. public void compute6(DependentTypesViewpointAdaptationTest other) { - // :: warning: (assignment) - @SameLen("this") DependentTypesViewpointAdaptationTest myOther = other; + DependentTypesViewpointAdaptationTest myOther = other; } } From 6b54b61513635cc8a33c0f3543bd8b4f92de962f Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Sun, 22 Oct 2023 16:19:08 -0700 Subject: [PATCH 3/6] Early exit, and handle varargs --- ...holeProgramInferenceJavaParserStorage.java | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java index be7faeb78a7..00b5aaaf2d1 100644 --- a/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java +++ b/framework/src/main/java/org/checkerframework/common/wholeprograminference/WholeProgramInferenceJavaParserStorage.java @@ -1077,7 +1077,12 @@ public void writeResultsToFile(OutputFormat outputFormat, BaseTypeChecker checke * @return true if the annotation might be relevant */ boolean annotationIsRelevant(AnnotationExpr anno) { - // This is a fully-qualified name. + GenericAnnotatedTypeFactory gatf = (GenericAnnotatedTypeFactory) atypeFactory; + if (gatf.relevantJavaTypes == null) { + return true; + } + + // `aname` is a fully-qualified name. String aName = anno.getNameAsString(); if (!atypeFactory.isSupportedQualifier(aName)) { // The annotation might be a declaration qualifier, such as a side effect specification. @@ -1086,27 +1091,34 @@ boolean annotationIsRelevant(AnnotationExpr anno) { Node parentNode = anno.getParentNode().get(); if (parentNode instanceof ArrayType) { - return ((GenericAnnotatedTypeFactory) atypeFactory).arraysAreRelevant(); + return gatf.arraysAreRelevant(); } if (parentNode instanceof ClassOrInterfaceType) { ClassOrInterfaceType classType = (ClassOrInterfaceType) parentNode; String simpleName = classType.getName().toString(); String scopedName = classType.getNameWithScope(); // TODO: Do I need to remove type parameters? - return ((GenericAnnotatedTypeFactory) atypeFactory).isRelevant(simpleName) - || ((GenericAnnotatedTypeFactory) atypeFactory).isRelevant(scopedName); + return gatf.isRelevant(simpleName) || gatf.isRelevant(scopedName); } if (parentNode instanceof IntersectionType) { return true; // TODO } + if (parentNode instanceof Parameter) { + Parameter param = (Parameter) parentNode; + if (param.isVarArgs()) { + return gatf.arraysAreRelevant(); + } else { + throw new Error("Unexpected type annotation on non-varargs Parameter: " + param); + } + } if (parentNode instanceof PrimitiveType) { - return ((GenericAnnotatedTypeFactory) atypeFactory).isRelevant(parentNode.toString()); + return gatf.isRelevant(parentNode.toString()); } if (parentNode instanceof UnionType) { return true; // TODO } if (parentNode instanceof VarType) { - return ((GenericAnnotatedTypeFactory) atypeFactory).nonprimitivesAreRelevant(); + return gatf.nonprimitivesAreRelevant(); } if (parentNode instanceof WildcardType) { return true; // TODO From b947f161b7d9e8fc1eedc94cb9fecc60831ca357 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 23 Oct 2023 11:19:38 -0700 Subject: [PATCH 4/6] Remove `@RelevantJavaTypes` annotation from `SameLenChecker` --- .../checker/index/samelen/SameLenChecker.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/checker/src/main/java/org/checkerframework/checker/index/samelen/SameLenChecker.java b/checker/src/main/java/org/checkerframework/checker/index/samelen/SameLenChecker.java index 1d8976036e1..97394dd83e5 100644 --- a/checker/src/main/java/org/checkerframework/checker/index/samelen/SameLenChecker.java +++ b/checker/src/main/java/org/checkerframework/checker/index/samelen/SameLenChecker.java @@ -1,7 +1,6 @@ package org.checkerframework.checker.index.samelen; import org.checkerframework.common.basetype.BaseTypeChecker; -import org.checkerframework.framework.qual.RelevantJavaTypes; import org.checkerframework.framework.source.SuppressWarningsPrefix; /** @@ -10,7 +9,9 @@ * * @checker_framework.manual #index-checker Index Checker */ -@RelevantJavaTypes({CharSequence.class, Object[].class, Object.class}) +// This @RelevantJavaTypes annotation is incorrect, because @SameLen can apply to an arbitrary +// user-defined datatype: https://checkerframework.org/manual/#index-annotating-fixed-size . +// @RelevantJavaTypes({CharSequence.class, Object[].class, Object.class}) @SuppressWarningsPrefix({"index", "samelen"}) public class SameLenChecker extends BaseTypeChecker { /** Create a new SameLenChecker. */ From 9f4523ba31b4d565f8bbac41fd2a59ccc5234b0d Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 23 Oct 2023 11:24:44 -0700 Subject: [PATCH 5/6] Undo a change --- .../DependentTypesViewpointAdaptationTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/checker/tests/ainfer-index/non-annotated/DependentTypesViewpointAdaptationTest.java b/checker/tests/ainfer-index/non-annotated/DependentTypesViewpointAdaptationTest.java index 9ce10b617ad..93d925e3b4c 100644 --- a/checker/tests/ainfer-index/non-annotated/DependentTypesViewpointAdaptationTest.java +++ b/checker/tests/ainfer-index/non-annotated/DependentTypesViewpointAdaptationTest.java @@ -59,11 +59,13 @@ public static void testThisInference( public void compute5( DependentTypesViewpointAdaptationTest this, DependentTypesViewpointAdaptationTest other) { - DependentTypesViewpointAdaptationTest myOther = other; + // :: warning: (assignment) + @SameLen("this") DependentTypesViewpointAdaptationTest myOther = other; } // Same as compute5, but without an explicit this parameter. public void compute6(DependentTypesViewpointAdaptationTest other) { - DependentTypesViewpointAdaptationTest myOther = other; + // :: warning: (assignment) + @SameLen("this") DependentTypesViewpointAdaptationTest myOther = other; } } From 8197f240170d13207b44fda0edcea1dfdfcb8fb4 Mon Sep 17 00:00:00 2001 From: Michael Ernst Date: Mon, 23 Oct 2023 12:10:48 -0700 Subject: [PATCH 6/6] Update test --- checker/tests/index/SameLenIrrelevant.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/checker/tests/index/SameLenIrrelevant.java b/checker/tests/index/SameLenIrrelevant.java index b440dbaca30..c59ceefe46a 100644 --- a/checker/tests/index/SameLenIrrelevant.java +++ b/checker/tests/index/SameLenIrrelevant.java @@ -1,20 +1,24 @@ // Tests that adding an @SameLen annotation to a primitive type is still // an error. +// All the errors in this test case are disabled. They were issued when `@SameLen` was restricted +// to arrays and CharSequence, but @SameLen can be written on an arbitrary user-defined type: +// https://checkerframework.org/manual/#index-annotating-fixed-size . + import org.checkerframework.checker.index.qual.SameLen; public class SameLenIrrelevant { - // :: error: (anno.on.irrelevant) + // NO :: error: (anno.on.irrelevant) public void test(@SameLen("#2") int x, int y) { // do nothing } - // :: error: (anno.on.irrelevant) + // NO :: error: (anno.on.irrelevant) public void test(@SameLen("#2") double x, double y) { // do nothing } - // :: error: (anno.on.irrelevant) + // NO :: error: (anno.on.irrelevant) public void test(@SameLen("#2") char x, char y) { // do nothing }