diff options
author | Stephan Herrmann | 2019-08-17 11:46:05 +0000 |
---|---|---|
committer | Stephan Herrmann | 2019-08-17 21:38:41 +0000 |
commit | 0b88ea1055cc55a836dcd4fe5e59ad3e00164797 (patch) | |
tree | 666d5a18053e48285b5f97db3733d49febcbf7a5 | |
parent | 372c6048cc1b21d8a4ceb948bf5eaecaa40576c9 (diff) | |
download | eclipse.jdt.core-0b88ea1055cc55a836dcd4fe5e59ad3e00164797.tar.gz eclipse.jdt.core-0b88ea1055cc55a836dcd4fe5e59ad3e00164797.tar.xz eclipse.jdt.core-0b88ea1055cc55a836dcd4fe5e59ad3e00164797.zip |
Bug 471009 - [null] extend "Annotate" command, to work on typeI20190817-1800
parameters
Change-Id: I5b52cfe8d9e7500e0c8fe1b0d8db1aeb053a22b3
7 files changed, 558 insertions, 33 deletions
diff --git a/org.eclipse.jdt.core.tests.builder/src/org/eclipse/jdt/core/tests/builder/IncrementalTests18.java b/org.eclipse.jdt.core.tests.builder/src/org/eclipse/jdt/core/tests/builder/IncrementalTests18.java index 1dd42e9d83..7cd8a8c7b9 100644 --- a/org.eclipse.jdt.core.tests.builder/src/org/eclipse/jdt/core/tests/builder/IncrementalTests18.java +++ b/org.eclipse.jdt.core.tests.builder/src/org/eclipse/jdt/core/tests/builder/IncrementalTests18.java @@ -599,8 +599,8 @@ public class IncrementalTests18 extends BuilderTests { projectPath, "Problem : Dead code [" + " resource : </Project/src/testNullAnnotations/NonNullUtils.java> range : <145,202> category : <90> severity : <1>]\n" + - "Problem : Null type mismatch (type annotations): required '@NonNull Object @NonNull[]' but this expression has type '@NonNull Object @Nullable[]' [" + - " resource : </Project/src/testNullAnnotations/Snippet.java> range : <316,323> category : <90> severity : <2>]"); + "Problem : Null type mismatch: required \'@NonNull Object @NonNull[]\' but the provided value is null [" + + " resource : </Project/src/testNullAnnotations/Snippet.java> range : <316,323> category : <90> severity : <2>]"); } public void testBug481276b() throws Exception { diff --git a/org.eclipse.jdt.core.tests.model/JCL/jclMin1.8.jar b/org.eclipse.jdt.core.tests.model/JCL/jclMin1.8.jar Binary files differindex 0e017da1d4..d7e520134e 100644 --- a/org.eclipse.jdt.core.tests.model/JCL/jclMin1.8.jar +++ b/org.eclipse.jdt.core.tests.model/JCL/jclMin1.8.jar diff --git a/org.eclipse.jdt.core.tests.model/JCL/jclMin1.8src.zip b/org.eclipse.jdt.core.tests.model/JCL/jclMin1.8src.zip Binary files differindex c85ea263d4..aed574c89d 100644 --- a/org.eclipse.jdt.core.tests.model/JCL/jclMin1.8src.zip +++ b/org.eclipse.jdt.core.tests.model/JCL/jclMin1.8src.zip diff --git a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ExternalAnnotations18Test.java b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ExternalAnnotations18Test.java index 002f1efaec..420e391b28 100644 --- a/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ExternalAnnotations18Test.java +++ b/org.eclipse.jdt.core.tests.model/src/org/eclipse/jdt/core/tests/model/ExternalAnnotations18Test.java @@ -53,10 +53,12 @@ import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.IMethodBinding; +import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.core.dom.IVariableBinding; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.NodeFinder; import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.TypeDeclaration; import org.eclipse.jdt.core.tests.util.AbstractCompilerTest; import org.eclipse.jdt.core.tests.util.Util; import org.eclipse.jdt.core.util.ExternalAnnotationUtil; @@ -1146,7 +1148,7 @@ public class ExternalAnnotations18Test extends ModifyingResourceTests { CompilationUnit unit = (CompilationUnit) parser.createAST(null); libWorkingCopy.discardWorkingCopy(); - // find type binding: + // find method binding: int start = lib1Content.indexOf("U string"); ASTNode name = NodeFinder.perform(unit, start, 0); assertTrue("should be simple name", name.getNodeType() == ASTNode.SIMPLE_NAME); @@ -1195,6 +1197,282 @@ public class ExternalAnnotations18Test extends ModifyingResourceTests { readFully(annotationFile)); } + public void testAnnotateMethodTypeParameter1() throws Exception { + myCreateJavaProject("TestLibs"); + String lib1Content = + "package libs;\n" + + "import java.util.List;\n" + + "\n" + + "public abstract class Lib1 {\n" + + " public abstract <S extends Throwable, T, U extends List<T>, V> U method(S s, T t, V v);\n" + + "}\n"; + addLibraryWithExternalAnnotations(this.project, "lib1.jar", "annots", new String[] { + "/UnannotatedLib/libs/Lib1.java", + lib1Content + }, null); + + // type check sources: + IPackageFragment fragment = this.project.getPackageFragmentRoots()[0].createPackageFragment("tests", true, null); + ICompilationUnit cu = fragment.createCompilationUnit("Test1.java", + "package tests;\n" + + "import org.eclipse.jdt.annotation.*;\n" + + "import libs.Lib1;\n" + + "\n" + + "public class Test1 {\n" + + " @NonNull Object test0(Lib1 xLib1) {\n" + + " return xLib1.method(\n" + + " (Error) null, this, xLib1);\n" + + " }\n" + + "}\n", + true, new NullProgressMonitor()).getWorkingCopy(new NullProgressMonitor()); + CompilationUnit reconciled = cu.reconcile(getJLS8(), true, null, new NullProgressMonitor()); + assertProblems(reconciled.getProblems(), new String[] { + "Pb(981) Unsafe interpretation of method return type as '@NonNull' based on substitution 'U=@NonNull List<Test1>'. Declaring type 'Lib1' doesn't seem to be designed with null type annotations in mind", + }, new int[] { 7 }); + + // acquire library AST: + IType type = this.project.findType("libs.Lib1"); + ICompilationUnit libWorkingCopy = type.getClassFile().getWorkingCopy(this.wcOwner, null); + ASTParser parser = ASTParser.newParser(getJLS8()); + parser.setSource(libWorkingCopy); + parser.setResolveBindings(true); + parser.setStatementsRecovery(false); + parser.setBindingsRecovery(false); + CompilationUnit unit = (CompilationUnit) parser.createAST(null); + libWorkingCopy.discardWorkingCopy(); + + // find method binding: + int start = lib1Content.indexOf("method"); + ASTNode name = NodeFinder.perform(unit, start, 0); + assertTrue("should be simple name", name.getNodeType() == ASTNode.SIMPLE_NAME); + ASTNode method = name.getParent(); + IMethodBinding methodBinding = ((MethodDeclaration)method).resolveBinding(); + + // find annotation file (not yet existing): + IFile annotationFile = ExternalAnnotationUtil.getAnnotationFile(this.project, methodBinding.getDeclaringClass(), null); + assertFalse("file should not exist", annotationFile.exists()); + assertEquals("file path", "/TestLibs/annots/libs/Lib1.eea", annotationFile.getFullPath().toString()); + + // annotate: + String originalSignature = ExternalAnnotationUtil.extractGenericSignature(methodBinding); + ExternalAnnotationUtil.annotateMethodTypeParameter("libs/Lib1", annotationFile, + "method", + originalSignature, + "1U::Ljava/util/List<TT;>;", // <- @NonNull U + 2, // annotate 3rd type parameter (U) + MergeStrategy.OVERWRITE_ANNOTATIONS, null); + assertTrue("file should exist", annotationFile.exists()); + + // check that the error is resolved now: + reconciled = cu.reconcile(getJLS8(), true, null, new NullProgressMonitor()); + assertNoProblems(reconciled.getProblems()); + + // add one more annotation: + ExternalAnnotationUtil.annotateMethodTypeParameter("libs/Lib1", annotationFile, + "method", + originalSignature, + "1S:Ljava/lang/Throwable;", // <- @NonNull S + 0, // annotate 1st type parameter (S) + MergeStrategy.OVERWRITE_ANNOTATIONS, null); + + // check that we have a new error now: + reconciled = cu.reconcile(getJLS8(), true, null, new NullProgressMonitor()); + assertProblems(reconciled.getProblems(), new String[] { + "Pb(910) Null type mismatch: required '@NonNull Error' but the provided value is null", + }, new int[] { 8 }); + + assertEquals("file content", + "class libs/Lib1\n" + + "method\n" + + " <S:Ljava/lang/Throwable;T:Ljava/lang/Object;U::Ljava/util/List<TT;>;V:Ljava/lang/Object;>(TS;TT;TV;)TU;\n" + + " <1S:Ljava/lang/Throwable;T:Ljava/lang/Object;1U::Ljava/util/List<TT;>;V:Ljava/lang/Object;>(TS;TT;TV;)TU;\n", + readFully(annotationFile)); + } + + public void testAnnotateMethodTypeParameter2() throws Exception { + myCreateJavaProject("TestLibs"); + String lib1Content = + "package libs;\n" + + "import java.util.List;\n" + + "\n" + + "public abstract class Entry<KK,VV> {\n" + + " public static <K, V extends Comparable<? super V>> Comparator<Entry<K,V>> comparingByValue() { return null; }\n" + + "}\n"; + addLibraryWithExternalAnnotations(this.project, "lib1.jar", "annots", new String[] { + "/UnannotatedLib/libs/Comparator.java", + "package libs;\n" + + "public class Comparator<T> {}\n", + "/UnannotatedLib/libs/Entry.java", + lib1Content + }, null); + + // acquire library AST: + IType type = this.project.findType("libs.Entry"); + ICompilationUnit libWorkingCopy = type.getClassFile().getWorkingCopy(this.wcOwner, null); + ASTParser parser = ASTParser.newParser(getJLS8()); + parser.setSource(libWorkingCopy); + parser.setResolveBindings(true); + parser.setStatementsRecovery(false); + parser.setBindingsRecovery(false); + CompilationUnit unit = (CompilationUnit) parser.createAST(null); + libWorkingCopy.discardWorkingCopy(); + + // find method binding: + int start = lib1Content.indexOf("comparingByValue"); + ASTNode name = NodeFinder.perform(unit, start, 0); + assertTrue("should be simple name", name.getNodeType() == ASTNode.SIMPLE_NAME); + ASTNode method = name.getParent(); + IMethodBinding methodBinding = ((MethodDeclaration)method).resolveBinding(); + + // find annotation file (not yet existing): + IFile annotationFile = ExternalAnnotationUtil.getAnnotationFile(this.project, methodBinding.getDeclaringClass(), null); + assertFalse("file should not exist", annotationFile.exists()); + assertEquals("file path", "/TestLibs/annots/libs/Entry.eea", annotationFile.getFullPath().toString()); + + // annotate: + String originalSignature = ExternalAnnotationUtil.extractGenericSignature(methodBinding); + // preview: + String[] annotatedSign = ExternalAnnotationUtil.annotateTypeParameter( + originalSignature, + "1K:Ljava/lang/Object;", // <- @NonNull K + 0, + MergeStrategy.OVERWRITE_ANNOTATIONS); + assertEquals("dry-run result", "[<, " + + "K:Ljava/lang/Object;, " + + "1K:Ljava/lang/Object;, " + // <- K + "V::Ljava/lang/Comparable<-TV;>;>()Llibs/Comparator<Llibs/Entry<TK;TV;>;>;]", + Arrays.toString(annotatedSign)); + // perform: + ExternalAnnotationUtil.annotateMethodTypeParameter("libs/Entry", annotationFile, + "comparingByValue", + originalSignature, + "1K:Ljava/lang/Object;", // <- @NonNull K + 0, // annotate 1st type parameter (K) + MergeStrategy.OVERWRITE_ANNOTATIONS, null); + assertTrue("file should exist", annotationFile.exists()); + + // type check sources: + IPackageFragment fragment = this.project.getPackageFragmentRoots()[0].createPackageFragment("tests", true, null); + ICompilationUnit cu = fragment.createCompilationUnit("Test1.java", + "package tests;\n" + + "import org.eclipse.jdt.annotation.*;\n" + + "import libs.*;\n" + + "\n" + + "public class Test1 {\n" + + " Object test0() {\n" + + " Comparator<Entry<@Nullable String,String>> c = Entry.comparingByValue();\n" + + " return c;\n" + + " }\n" + + "}\n", + true, new NullProgressMonitor()).getWorkingCopy(new NullProgressMonitor()); + CompilationUnit reconciled = cu.reconcile(getJLS8(), true, null, new NullProgressMonitor()); + assertProblems(reconciled.getProblems(), new String[] { + "Pb(953) Null type mismatch (type annotations): required 'Comparator<Entry<@Nullable String,String>>' but this expression has type 'Comparator<Entry<@NonNull String,String>>'", + }, new int[] { 7 }); + + // un-annotate: + annotatedSign = ExternalAnnotationUtil.annotateTypeParameter( + originalSignature, + "@K:Ljava/lang/Object;", // <- <del>1</del> K + 0, MergeStrategy.OVERWRITE_ANNOTATIONS); + assertEquals("dry-run result", "[<, " + + "K:Ljava/lang/Object;, " + + "K:Ljava/lang/Object;, " + // <- K + "V::Ljava/lang/Comparable<-TV;>;>()Llibs/Comparator<Llibs/Entry<TK;TV;>;>;]", + Arrays.toString(annotatedSign)); + } + + public void testAnnotateClassTypeParameter1() throws Exception { + myCreateJavaProject("TestLibs"); + String lib1Content = + "package libs;\n" + + "import java.util.List;\n" + + "\n" + + "public abstract class Lib1 <S extends Throwable, T, U extends List<T>, V> {\n" + + " public abstract U method(S s, T t, V v);\n" + + "}\n"; + addLibraryWithExternalAnnotations(this.project, "lib1.jar", "annots", new String[] { + "/UnannotatedLib/libs/Lib1.java", + lib1Content + }, null); + + // type check sources: + IPackageFragment fragment = this.project.getPackageFragmentRoots()[0].createPackageFragment("tests", true, null); + ICompilationUnit cu = fragment.createCompilationUnit("Test1.java", + "package tests;\n" + + "import org.eclipse.jdt.annotation.*;\n" + + "import libs.Lib1;\n" + + "import java.util.List;\n" + + "\n" + + "public class Test1 {\n" + + " @NonNull List<Test1> test0(Lib1<Error,Test1,@NonNull List<Test1>,String> xLib1) {\n" + + " return xLib1.method(\n" + + " (Error) null, this, \"1\");\n" + + " }\n" + + "}\n", + true, new NullProgressMonitor()).getWorkingCopy(new NullProgressMonitor()); + CompilationUnit reconciled = cu.reconcile(getJLS8(), true, null, new NullProgressMonitor()); + assertProblems(reconciled.getProblems(), new String[] { + "Pb(980) Unsafe interpretation of method return type as '@NonNull' based on the receiver type 'Lib1<Error,Test1,@NonNull List<Test1>,String>'. Type 'Lib1<S,T,U,V>' doesn't seem to be designed with null type annotations in mind", + }, new int[] { 8 }); + + // acquire library AST: + IType type = this.project.findType("libs.Lib1"); + ICompilationUnit libWorkingCopy = type.getClassFile().getWorkingCopy(this.wcOwner, null); + ASTParser parser = ASTParser.newParser(getJLS8()); + parser.setSource(libWorkingCopy); + parser.setResolveBindings(true); + parser.setStatementsRecovery(false); + parser.setBindingsRecovery(false); + CompilationUnit unit = (CompilationUnit) parser.createAST(null); + libWorkingCopy.discardWorkingCopy(); + + // find type binding: + int start = lib1Content.indexOf("Lib1"); + ASTNode name = NodeFinder.perform(unit, start, 0); + assertTrue("should be simple name", name.getNodeType() == ASTNode.SIMPLE_NAME); + ASTNode typeDecl = name.getParent(); + ITypeBinding typeBinding = ((TypeDeclaration)typeDecl).resolveBinding(); + + // find annotation file (not yet existing): + IFile annotationFile = ExternalAnnotationUtil.getAnnotationFile(this.project, typeBinding, null); + assertFalse("file should not exist", annotationFile.exists()); + assertEquals("file path", "/TestLibs/annots/libs/Lib1.eea", annotationFile.getFullPath().toString()); + + // annotate: + String originalSignature = ExternalAnnotationUtil.extractGenericTypeParametersSignature(typeBinding); + ExternalAnnotationUtil.annotateTypeTypeParameter("libs/Lib1", annotationFile, + originalSignature, + "1U::Ljava/util/List<TT;>;", // <- @NonNull U + 2, // annotate 3rd type parameter (U) + MergeStrategy.OVERWRITE_ANNOTATIONS, null); + assertTrue("file should exist", annotationFile.exists()); + + // check that the error is resolved now: + reconciled = cu.reconcile(getJLS8(), true, null, new NullProgressMonitor()); + assertNoProblems(reconciled.getProblems()); + + // add one more annotation: + ExternalAnnotationUtil.annotateTypeTypeParameter("libs/Lib1", annotationFile, + originalSignature, + "1S:Ljava/lang/Throwable;", // <- @NonNull S + 0, // annotate 1st type parameter (S) + MergeStrategy.OVERWRITE_ANNOTATIONS, null); + + // check that we have a new error now: + reconciled = cu.reconcile(getJLS8(), true, null, new NullProgressMonitor()); + assertProblems(reconciled.getProblems(), new String[] { + "Pb(964) Null constraint mismatch: The type 'Error' is not a valid substitute for the type parameter '@NonNull S extends Throwable'", + "Pb(910) Null type mismatch: required '@NonNull Error' but the provided value is null", + }, new int[] { 7, 9 }); + + assertEquals("file content", + "class libs/Lib1\n" + + " <S:Ljava/lang/Throwable;T:Ljava/lang/Object;U::Ljava/util/List<TT;>;V:Ljava/lang/Object;>\n" + + " <1S:Ljava/lang/Throwable;T:Ljava/lang/Object;1U::Ljava/util/List<TT;>;V:Ljava/lang/Object;>\n", + readFully(annotationFile)); + } + // ===== white box tests for ExternalAnnotationUtil ===== public void testBug470666a() throws CoreException, IOException { diff --git a/org.eclipse.jdt.core/compiler/org/eclipse/jdt/internal/compiler/ast/NullAnnotationMatching.java b/org.eclipse.jdt.core/compiler/org/eclipse/jdt/internal/compiler/ast/NullAnnotationMatching.java index abda6fd7fd..bf6658821b 100644 --- a/org.eclipse.jdt.core/compiler/org/eclipse/jdt/internal/compiler/ast/NullAnnotationMatching.java +++ b/org.eclipse.jdt.core/compiler/org/eclipse/jdt/internal/compiler/ast/NullAnnotationMatching.java @@ -302,8 +302,11 @@ public class NullAnnotationMatching { } } severity = severity.max(dimSeverity); - if (severity == Severity.MISMATCH) + if (severity == Severity.MISMATCH) { + if (nullStatus == FlowInfo.NULL) + return new NullAnnotationMatching(severity, nullStatus, null); return NullAnnotationMatching.NULL_ANNOTATIONS_MISMATCH; + } } if (severity == Severity.OK) nullStatus = -1; @@ -568,6 +571,9 @@ public class NullAnnotationMatching { } else if (requiredBits == TagBits.AnnotationNonNull) { switch (mode) { case COMPATIBLE: + if (nullStatus == FlowInfo.NULL) + return Severity.MISMATCH; // NOK by flow analysis + //$FALL-THROUGH$ case BOUND_SUPER_CHECK: if (nullStatus == FlowInfo.NON_NULL) return Severity.OK; // OK by flow analysis diff --git a/org.eclipse.jdt.core/compiler/org/eclipse/jdt/internal/compiler/lookup/SignatureWrapper.java b/org.eclipse.jdt.core/compiler/org/eclipse/jdt/internal/compiler/lookup/SignatureWrapper.java index 9425c40ea9..4e4fa4b504 100644 --- a/org.eclipse.jdt.core/compiler/org/eclipse/jdt/internal/compiler/lookup/SignatureWrapper.java +++ b/org.eclipse.jdt.core/compiler/org/eclipse/jdt/internal/compiler/lookup/SignatureWrapper.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2016 IBM Corporation and others. + * Copyright (c) 2000, 2019 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -145,6 +145,20 @@ public class SignatureWrapper { } return i; } + public int skipTypeParameter() { + // [Annot] Identifier ClassBound {InterfaceBound} + this.start = CharOperation.indexOf(':', this.signature, this.start); + while (charAtStart() == ':') { + this.start++; + if (charAtStart() != ':') // ClassBound may be empty + this.start = skipAngleContents(computeEnd()) + 1; + } + return this.start; + } + public char[] wordUntil(char c) { + this.end = CharOperation.indexOf(c, this.signature, this.start); + return CharOperation.subarray(this.signature, this.start, this.start = this.end); // skip word + } public char[] nextWord() { this.end = CharOperation.indexOf(';', this.signature, this.start); if (this.bracket <= this.start) // already know it if its > start diff --git a/org.eclipse.jdt.core/model/org/eclipse/jdt/core/util/ExternalAnnotationUtil.java b/org.eclipse.jdt.core/model/org/eclipse/jdt/core/util/ExternalAnnotationUtil.java index 301b4e5bd5..67dba5de30 100644 --- a/org.eclipse.jdt.core/model/org/eclipse/jdt/core/util/ExternalAnnotationUtil.java +++ b/org.eclipse.jdt.core/model/org/eclipse/jdt/core/util/ExternalAnnotationUtil.java @@ -39,6 +39,7 @@ import org.eclipse.jdt.core.compiler.CharOperation; import org.eclipse.jdt.core.dom.IMethodBinding; import org.eclipse.jdt.core.dom.ITypeBinding; import org.eclipse.jdt.internal.compiler.classfmt.ExternalAnnotationProvider; +import org.eclipse.jdt.internal.compiler.codegen.ConstantPool; import org.eclipse.jdt.internal.compiler.lookup.SignatureWrapper; import org.eclipse.jdt.internal.core.ClasspathEntry; import org.eclipse.jdt.internal.core.util.KeyToSignature; @@ -76,6 +77,7 @@ public final class ExternalAnnotationUtil { private static final int POSITION_RETURN_TYPE = -1; private static final int POSITION_FULL_SIGNATURE = -2; + private static final int POSITION_TYPE_PARAMETER = -3; /** * Answer the give method's signature in class file format. @@ -101,6 +103,36 @@ public final class ExternalAnnotationUtil { } /** + * Answer the signature of all type parameters of this type in class file format. + * @param type binding representing a type, required to have type parameters + * @return a signature in class file format + * @since 3.19 + */ + public static String extractGenericTypeParametersSignature(ITypeBinding type) { + StringBuilder signature = new StringBuilder().append('<'); + for (ITypeBinding typeParameter : type.getTypeParameters()) { + signature.append(typeParameter.getName()); + // superclass (if relevant, else empty ':'): + signature.append(':'); + ITypeBinding superclass = typeParameter.getSuperclass(); + if (superclass != null) { + String superclassSignature = extractGenericTypeSignature(superclass); + boolean superIsRelevant = typeParameter.getInterfaces().length == 0; + superIsRelevant |= !superclassSignature.equals(new String(ConstantPool.JavaLangObjectSignature)); + if (superIsRelevant) { + signature.append(superclassSignature); + } + } + // superinterfaces: + for (ITypeBinding superInterface : typeParameter.getInterfaces()) { + signature.append(':').append(extractGenericTypeSignature(superInterface)); + } + } + signature.append('>'); + return signature.toString(); + } + + /** * Insert an encoded annotation into the given methodSignature affecting its return type. * <p> * This method is suitable for declaration annotations. @@ -217,6 +249,59 @@ public final class ExternalAnnotationUtil { } /** + * Update the given external annotation file with details regarding annotations of a type parameter of the type itself. + * If the type already has external annotations, old and new annotations will be merged, + * with priorities controlled by the parameter 'mergeStrategy'. + * <p> + * This method is suitable only for type use annotations. + * </p> + * @param typeName binary name (slash separated) of the type being annotated + * @param file a file assumed to be in .eea format, will be created if it doesn't exist. + * @param originalSignature unannotated signature of all type parameters of the type + * @param annotatedTypeParameter signature of the new type parameter whose annotations should be superimposed on the method + * @param rank rank of the type parameter to be annotated + * @param mergeStrategy controls how old and new signatures should be merged + * @param monitor progress monitor to be passed through into file operations, or null if no reporting is desired + * @throws CoreException if access to the file fails + * @throws IOException if reading file content fails + * @throws IllegalArgumentException if the annotatedTypeParameter does not structurally match to originalSignature + * @since 3.19 + */ + public static void annotateTypeTypeParameter(String typeName, IFile file, String originalSignature, String annotatedTypeParameter, + int rank, MergeStrategy mergeStrategy, IProgressMonitor monitor) + throws CoreException, IOException, IllegalArgumentException + { + annotateMember(typeName, file, null, originalSignature, annotatedTypeParameter, POSITION_TYPE_PARAMETER-rank, mergeStrategy, monitor); + } + + /** + * Update the given external annotation file with details regarding annotations of a type parameter of a given method. + * If the specified method already has external annotations, old and new annotations will be merged, + * with priorities controlled by the parameter 'mergeStrategy'. + * <p> + * This method is suitable only for type use annotations. + * </p> + * @param typeName binary name (slash separated) of the type being annotated + * @param file a file assumed to be in .eea format, will be created if it doesn't exist. + * @param selector selector of the method + * @param originalSignature unannotated signature of the member, used for identification + * @param annotatedTypeParameter signature of the new type parameter whose annotations should be superimposed on the method + * @param rank rank of the type parameter to be annotated + * @param mergeStrategy controls how old and new signatures should be merged + * @param monitor progress monitor to be passed through into file operations, or null if no reporting is desired + * @throws CoreException if access to the file fails + * @throws IOException if reading file content fails + * @throws IllegalArgumentException if the annotatedTypeParameter does not structurally match to originalSignature + * @since 3.19 + */ + public static void annotateMethodTypeParameter(String typeName, IFile file, String selector, String originalSignature, String annotatedTypeParameter, + int rank, MergeStrategy mergeStrategy, IProgressMonitor monitor) + throws CoreException, IOException, IllegalArgumentException + { + annotateMember(typeName, file, selector, originalSignature, annotatedTypeParameter, POSITION_TYPE_PARAMETER-rank, mergeStrategy, monitor); + } + + /** * Update the given external annotation file with details regarding annotations of the return type of a given method. * If the specified method already has external annotations, old and new annotations will be merged, * with priorities controlled by the parameter 'mergeStrategy'. @@ -281,7 +366,9 @@ public final class ExternalAnnotationUtil { newContent.append(ExternalAnnotationProvider.CLASS_PREFIX); newContent.append(typeName).append('\n'); // new entry: - newContent.append(selector).append('\n'); + if (selector != null) { // otherwise we are annotating a class type parameter + newContent.append(selector).append('\n'); + } newContent.append(' ').append(originalSignature).append('\n'); newContent.append(' ').append(annotatedSignature).append('\n'); @@ -290,9 +377,22 @@ public final class ExternalAnnotationUtil { BufferedReader reader = new BufferedReader(new InputStreamReader(file.getContents())); StringBuffer newContent = new StringBuffer(); try { + // type references get the previous signature from the existing type binding: + String previousSignature = originalSignature; newContent.append(reader.readLine()).append('\n'); // skip class name String line; while ((line = reader.readLine()) != null) { + if (selector == null) { // annotating a type parameter? + if (line.trim().startsWith("<")) { //$NON-NLS-1$ + line = reader.readLine(); + assert line.trim().startsWith("<"); // reading old annotated type parameters //$NON-NLS-1$ + // bindings of type parameters (being declarations) don't include annotations in their signature, + // so we need to preserve the previous signature from the .eea: + previousSignature = line.trim(); + line = reader.readLine(); + } + break; + } if (line.isEmpty()) { newContent.append('\n'); continue; @@ -353,9 +453,10 @@ public final class ExternalAnnotationUtil { } } // add new entry: - newContent.append(selector).append('\n'); + if (selector != null) + newContent.append(selector).append('\n'); newContent.append(' ').append(originalSignature).append('\n'); - annotatedSignature = updateSignature(originalSignature, annotatedSignature, updatePosition, mergeStrategy); + annotatedSignature = updateSignature(previousSignature, annotatedSignature, updatePosition, mergeStrategy); writeFile(file, newContent, annotatedSignature, line, reader, monitor); } finally { reader.close(); @@ -367,28 +468,48 @@ public final class ExternalAnnotationUtil { StringBuffer buf = new StringBuffer(); String signatureToReplace; String postfix = null; - switch (updatePosition) { - case POSITION_FULL_SIGNATURE: - signatureToReplace = originalSignature; - break; - case POSITION_RETURN_TYPE: - assert originalSignature.charAt(0) == '(' || originalSignature.charAt(0) == '<': "signature must start with '(' or '<'"; //$NON-NLS-1$ - int close = originalSignature.indexOf(')'); - buf.append(originalSignature, 0, close+1); - signatureToReplace = originalSignature.substring(close+1); - break; - default: // parameter - SignatureWrapper wrapper = new SignatureWrapper(originalSignature.toCharArray(), true, true); // may already contain annotations - wrapper.start = CharOperation.indexOf('(', wrapper.signature) + 1; // possibly skipping type parameters - for (int i = 0; i < updatePosition; i++) - wrapper.start = wrapper.skipAngleContents(wrapper.computeEnd()) + 1; - int start = wrapper.start; - int end = wrapper.skipAngleContents(wrapper.computeEnd()); - buf.append(originalSignature, 0, start); - signatureToReplace = originalSignature.substring(start, end+1); - postfix = originalSignature.substring(end+1, originalSignature.length()); + if (updatePosition <= POSITION_TYPE_PARAMETER) { + // '<' [Annot] Identifier ClassBound {InterfaceBound} ... '>' + assert originalSignature.charAt(0) == '<': "generic signature must start with '<'"; //$NON-NLS-1$ + SignatureWrapper wrapper = new SignatureWrapper(originalSignature.toCharArray(), true, true); // may already contain annotations + wrapper.start = 1; // skip '<' + // skip preceding type parameters: + for (int i = 0; i < (-updatePosition+POSITION_TYPE_PARAMETER); i++) { + wrapper.skipTypeParameter(); + } + int start = wrapper.start; + // copy entire prefix: + buf.append(originalSignature, 0, start); + // process selected type parameter: + int end = wrapper.skipTypeParameter(); + signatureToReplace = originalSignature.substring(start, end); + updateTypeParameter(buf, signatureToReplace.toCharArray(), annotatedSignature.toCharArray(), mergeStrategy); + // extract postfix: + postfix = originalSignature.substring(end, originalSignature.length()); + } else { + switch (updatePosition) { + case POSITION_FULL_SIGNATURE: + signatureToReplace = originalSignature; + break; + case POSITION_RETURN_TYPE: + assert originalSignature.charAt(0) == '(' || originalSignature.charAt(0) == '<': "signature must start with '(' or '<'"; //$NON-NLS-1$ + int close = originalSignature.indexOf(')'); + buf.append(originalSignature, 0, close+1); + signatureToReplace = originalSignature.substring(close+1); + break; + default: // parameter + SignatureWrapper wrapper = new SignatureWrapper(originalSignature.toCharArray(), true, true); // may already contain annotations + wrapper.start = CharOperation.indexOf('(', wrapper.signature) + 1; // possibly skipping type parameters + for (int i = 0; i < updatePosition; i++) + wrapper.start = wrapper.skipAngleContents(wrapper.computeEnd()) + 1; + int start = wrapper.start; + int end = wrapper.skipAngleContents(wrapper.computeEnd()); + buf.append(originalSignature, 0, start); + signatureToReplace = originalSignature.substring(start, end+1); + postfix = originalSignature.substring(end+1, originalSignature.length()); + } + updateType(buf, signatureToReplace.toCharArray(), annotatedSignature.toCharArray(), mergeStrategy); } - updateType(buf, signatureToReplace.toCharArray(), annotatedSignature.toCharArray(), mergeStrategy); if (postfix != null) buf.append(postfix); return buf.toString(); @@ -461,6 +582,62 @@ public final class ExternalAnnotationUtil { return false; } /** + * similar to updateType() but for type parameters, syntax: + * [Annot] Identifier ClassBound {InterfaceBound} + */ + private static boolean updateTypeParameter(StringBuffer buf, char[] oldType, char[] newType, MergeStrategy mergeStrategy) { + if (mergeStrategy == MergeStrategy.REPLACE_SIGNATURE) { + buf.append(newType); + return false; + } + try { + SignatureWrapper oWrap = new SignatureWrapper(oldType, true, true); // may already contain annotations + SignatureWrapper nWrap = new SignatureWrapper(newType, true, true); // may already contain annotations + // [Annot] + mergeAnnotation(buf, oWrap, nWrap, mergeStrategy); + // Identifier: + char[] oName = oWrap.wordUntil(':'); + char[] nName = nWrap.wordUntil(':'); + if (!CharOperation.equals(oName, nName)) { + StringBuilder msg = new StringBuilder("Structural mismatch between type parameters ").append(oName).append(" and ").append(nName); //$NON-NLS-1$ //$NON-NLS-2$ + throw new IllegalArgumentException(msg.toString()); + } + buf.append(oName); + // one or more bounds each starting with ':' + while (match(buf, oWrap, nWrap, ':', false)) { + int oStart = oWrap.start; + int nStart = nWrap.start; + nWrap.skipAngleContents(nWrap.computeEnd()); + char[] nType = nWrap.getFrom(nStart); + char[] oType; + if (oWrap.charAtStart() == ':') { + // old bound is empty! + if (CharOperation.equals(nType, new char[]{':'})) { + nWrap.start--; // unget second ':' + continue; // both bounds empty + } + if (CharOperation.equals(nType, ConstantPool.JavaLangObjectSignature)) { + // new: j.l.Object => skip + continue; + } + // new is not exactly Ljava/lang/Object; (perhaps already annotated?) => replace old with j.l.Object for decoration: + oType = ConstantPool.JavaLangObjectSignature; + } else { + oWrap.skipAngleContents(oWrap.computeEnd()); + oType = oWrap.getFrom(oStart); + } + if (updateType(buf, oType, nType, mergeStrategy)) + mergeAnnotation(buf, oWrap, nWrap, mergeStrategy); + if (oWrap.atEnd() || nWrap.atEnd()) + return false; + } + } catch (ArrayIndexOutOfBoundsException aioobe) { // from several locations inside match() or mergeAnnotation(). + StringBuilder msg = new StringBuilder("Structural mismatch between ").append(oldType).append(" and ").append(newType); //$NON-NLS-1$ //$NON-NLS-2$ + throw new IllegalArgumentException(msg.toString(), aioobe); + } + return false; + } + /** * Does the current char at both given signatures match the 'expected' char? * If yes, print it into 'buf' and answer true. * If no, if 'force' raise an exception, else quietly answer false without updating 'buf'. @@ -572,7 +749,7 @@ public final class ExternalAnnotationUtil { * Retrieve the annotated signature of a specified member as found in the given external annotation file, if any. * @param typeName fully qualified slash-separated name of the type for which the file defines external annotations * @param file a file assumed to be in .eea format, must not be null, but may not exist - * @param selector name of the member whose annotation we are looking for + * @param selector name of the member whose annotation we are looking for, or null when annotating the type's type parameters * @param originalSignature the unannotated signature by which the member is identified * @return the annotated signature as found in the file, or null. */ @@ -583,9 +760,17 @@ public final class ExternalAnnotationUtil { while (true) { String line = reader.readLine(); // selector: - if (selector.equals(line)) { + if (selector != null) { + if (selector.equals(line)) { + // original signature: + line = reader.readLine(); + if (originalSignature.equals(ExternalAnnotationProvider.extractSignature(line))) { + // annotated signature: + return ExternalAnnotationProvider.extractSignature(reader.readLine()); + } + } + } else if (line != null && line.trim().startsWith("<")) { //$NON-NLS-1$ // original signature: - line = reader.readLine(); if (originalSignature.equals(ExternalAnnotationProvider.extractSignature(line))) { // annotated signature: return ExternalAnnotationProvider.extractSignature(reader.readLine()); @@ -664,7 +849,7 @@ public final class ExternalAnnotationUtil { * * @param originalSignature the original full signature, may be annotated already * @param annotatedType a type signature with additional annotations (incl. {@link #NO_ANNOTATION}). - * @param paramIdx the index of a parameter to annotated + * @param paramIdx the index of a parameter to annotate * @param mergeStrategy controls how old and new signatures should be merged * @return an array of length four: <ul> * <li>prefix up-to the changed type</li> @@ -691,4 +876,46 @@ public final class ExternalAnnotationUtil { result[3] = originalSignature.substring(end+1, originalSignature.length()); return result; } + + /** + * Apply the specified changes on a type parameter within the given signature. + * This method can be used as a dry run without modifying an annotation file. + * + * @param originalSignature the original full signature, may be annotated already + * @param annotatedType a type signature with additional annotations (incl. {@link #NO_ANNOTATION}). + * @param rank the index of a type parameter to annotate + * @param mergeStrategy controls how old and new signatures should be merged + * @return an array of length four: <ul> + * <li>prefix up-to the changed type</li> + * <li>original type</li> + * <li>changed type</li> + * <li>postfix after the changed type</li> + * </ul> + * @since 3.19 + */ + public static String[] annotateTypeParameter(String originalSignature, String annotatedType, int rank, MergeStrategy mergeStrategy) + { + String[] result = new String[4]; // prefix, orig, replacement, postfix + StringBuffer buf = new StringBuffer(); + SignatureWrapper wrapper = new SignatureWrapper(originalSignature.toCharArray(), true, true); // may already contain annotations + wrapper.start = 1; // skip '<' + // prefix: + for (int i = 0; i < rank; i++) { + wrapper.skipTypeParameter(); + } + int start = wrapper.start; + result[0] = originalSignature.substring(0, start); + // orig: + int end = wrapper.skipTypeParameter(); + result[1] = originalSignature.substring(start, end); + // replacement: + SignatureWrapper oWrap = new SignatureWrapper(result[1].toCharArray()); + SignatureWrapper nWrap = new SignatureWrapper(annotatedType.toCharArray()); + mergeAnnotation(buf, oWrap, nWrap, mergeStrategy); + updateTypeParameter(buf, oWrap.tail(), nWrap.tail(), mergeStrategy); + result[2] = buf.toString(); + // postfix: + result[3] = originalSignature.substring(end, originalSignature.length()); + return result; + } } |