Bug 471009 - [null] extend "Annotate" command, to work on type
parameters
Change-Id: I75d7b8379958143f344edc2482e9eb5d848272a2
diff --git a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/AnnotateAssistTest18.java b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/AnnotateAssistTest18.java
index 2c99047..5dcb999 100644
--- a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/AnnotateAssistTest18.java
+++ b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/quickfix/AnnotateAssistTest18.java
@@ -892,13 +892,11 @@
/**
* Assert two proposals ("@NonNull" and "@Nullable") on a method's type parameter
- * The parameterized type and the wildcard already has a @NonNull annotation.
- * Annotation entry already exists, with @NonNull on the wildcard itself.
* Apply the second proposal and check the effect.
+ * Then repeat for another type parameter (to check merging of changes)
* @throws Exception multiple causes
*/
- // FIXME(stephan): enable once implemented
- public void _testAnnotateParameter_TypeParameter() throws Exception {
+ public void testAnnotateMethod_TypeParameter1() throws Exception {
String X_PATH= "pack/age/X";
String[] pathAndContents= new String[] {
@@ -922,19 +920,19 @@
assertCorrectLabels(list);
assertNumberOfProposals(list, 2);
- ICompletionProposal proposal= findProposalByName("Annotate as '@NonNull T'", list);
+ ICompletionProposal proposal= findProposalByName("Annotate as '@NonNull T extends List<X>'", list);
String expectedInfo=
"<dl><dt>test</dt>" +
- "<dd><X:Ljava/lang/Object;T:Ljava/util/List<TX;>>(TT;)TX;</dd>" +
- "<dd><X:Ljava/lang/Object;1T:Ljava/util/List<TX;>>(TT;)TX;</dd>" + // <= 1
+ "<dd><X:Ljava/lang/Object;T::Ljava/util/List<TX;>;>(TT;)TX;</dd>" +
+ "<dd><X:Ljava/lang/Object;<b>1</b>T::Ljava/util/List<TX;>;>(TT;)TX;</dd>" + // <= 1
"</dl>";
assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
- proposal= findProposalByName("Annotate as '@Nullable Number'", list);
+ proposal= findProposalByName("Annotate as '@Nullable T extends List<X>'", list);
expectedInfo=
"<dl><dt>test</dt>" +
- "<dd><X:Ljava/lang/Object;T:Ljava/util/List<TX;>>(TT;)TX;</dd>" +
- "<dd><X:Ljava/lang/Object;0T:Ljava/util/List<TX;>>(TT;)TX;</dd>" + // <= 0
+ "<dd><X:Ljava/lang/Object;T::Ljava/util/List<TX;>;>(TT;)TX;</dd>" +
+ "<dd><X:Ljava/lang/Object;<b>0</b>T::Ljava/util/List<TX;>;>(TT;)TX;</dd>" + // <= 0
"</dl>";
assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
@@ -947,8 +945,143 @@
String expectedContent=
"class pack/age/X\n" +
"test\n" +
- " <X:Ljava/lang/Object;T:Ljava/util/List<;TX;>>(TT;)TX;\n" +
- " <X:Ljava/lang/Object;0T:Ljava/util/List<;TX;>>(TT;)TX;\n";
+ " <X:Ljava/lang/Object;T::Ljava/util/List<TX;>;>(TT;)TX;\n" +
+ " <X:Ljava/lang/Object;0T::Ljava/util/List<TX;>;>(TT;)TX;\n";
+ checkContentOfFile("annotation file content", annotationFile, expectedContent);
+
+ // add second annotation:
+ offset= pathAndContents[1].indexOf("X,");
+
+ list= collectAnnotateProposals(javaEditor, offset);
+
+ assertCorrectLabels(list);
+ assertNumberOfProposals(list, 2);
+
+ proposal= findProposalByName("Annotate as '@NonNull X'", list);
+ expectedInfo=
+ "<dl><dt>test</dt>" +
+ "<dd><X:Ljava/lang/Object;T::Ljava/util/List<TX;>;>(TT;)TX;</dd>" +
+ "<dd><<b>1</b>X:Ljava/lang/Object;0T::Ljava/util/List<TX;>;>(TT;)TX;</dd>" + // <= 1
+ "</dl>";
+ assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+ proposal= findProposalByName("Annotate as '@Nullable X'", list);
+ expectedInfo=
+ "<dl><dt>test</dt>" +
+ "<dd><X:Ljava/lang/Object;T::Ljava/util/List<TX;>;>(TT;)TX;</dd>" +
+ "<dd><<b>0</b>X:Ljava/lang/Object;0T::Ljava/util/List<TX;>;>(TT;)TX;</dd>" + // <= 0
+ "</dl>";
+ assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+ document= javaEditor.getDocumentProvider().getDocument(javaEditor.getEditorInput());
+ proposal.apply(document);
+
+ annotationFile= fJProject1.getProject().getFile(new Path(ANNOTATION_PATH).append(X_PATH+".eea"));
+ assertTrue("Annotation file should have been created", annotationFile.exists());
+
+ expectedContent=
+ "class pack/age/X\n" +
+ "test\n" +
+ " <X:Ljava/lang/Object;T::Ljava/util/List<TX;>;>(TT;)TX;\n" +
+ " <0X:Ljava/lang/Object;0T::Ljava/util/List<TX;>;>(TT;)TX;\n";
+ checkContentOfFile("annotation file content", annotationFile, expectedContent);
+ } finally {
+ JavaPlugin.getActivePage().closeAllEditors(false);
+ }
+ }
+
+ /**
+ * Assert two proposals ("@NonNull" and "@Nullable") on a type's type parameter
+ * Apply the second proposal and check the effect.
+ * Then repeat for another type parameter (to check merging of changes)
+ * @throws Exception multiple causes
+ */
+ public void testAnnotateMethod_TypeParameter2() throws Exception {
+
+ String X_PATH= "pack/age/X";
+ String[] pathAndContents= new String[] {
+ X_PATH+".java",
+ "package pack.age;\n" +
+ "import java.util.List;\n" +
+ "public interface X <X, T extends List<X>> {\n" +
+ " public X test(T list);\n" +
+ "}\n"
+ };
+ addLibrary(fJProject1, "lib.jar", "lib.zip", pathAndContents, ANNOTATION_PATH, JavaCore.VERSION_1_8, null);
+
+ IType type= fJProject1.findType(X_PATH.replace('/', '.'));
+ JavaEditor javaEditor= (JavaEditor) JavaUI.openInEditor(type);
+
+ try {
+ int offset= pathAndContents[1].indexOf("T extends");
+
+ List<ICompletionProposal> list= collectAnnotateProposals(javaEditor, offset);
+
+ assertCorrectLabels(list);
+ assertNumberOfProposals(list, 2);
+
+ ICompletionProposal proposal= findProposalByName("Annotate as '@NonNull T extends List<X>'", list);
+ String expectedInfo=
+ "<dl><dt>class pack/age/X</dt>" +
+ "<dd><X:Ljava/lang/Object;T::Ljava/util/List<TX;>;></dd>" +
+ "<dd><X:Ljava/lang/Object;<b>1</b>T::Ljava/util/List<TX;>;></dd>" + // <= 1
+ "</dl>";
+ assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+ proposal= findProposalByName("Annotate as '@Nullable T extends List<X>'", list);
+ expectedInfo=
+ "<dl><dt>class pack/age/X</dt>" +
+ "<dd><X:Ljava/lang/Object;T::Ljava/util/List<TX;>;></dd>" +
+ "<dd><X:Ljava/lang/Object;<b>0</b>T::Ljava/util/List<TX;>;></dd>" + // <= 0
+ "</dl>";
+ assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+ IDocument document= javaEditor.getDocumentProvider().getDocument(javaEditor.getEditorInput());
+ proposal.apply(document);
+
+ IFile annotationFile= fJProject1.getProject().getFile(new Path(ANNOTATION_PATH).append(X_PATH+".eea"));
+ assertTrue("Annotation file should have been created", annotationFile.exists());
+
+ String expectedContent=
+ "class pack/age/X\n" +
+ " <X:Ljava/lang/Object;T::Ljava/util/List<TX;>;>\n" +
+ " <X:Ljava/lang/Object;0T::Ljava/util/List<TX;>;>\n";
+ checkContentOfFile("annotation file content", annotationFile, expectedContent);
+
+ // add second annotation:
+ offset= pathAndContents[1].indexOf("X,");
+
+ list= collectAnnotateProposals(javaEditor, offset);
+
+ assertCorrectLabels(list);
+ assertNumberOfProposals(list, 2);
+
+ proposal= findProposalByName("Annotate as '@NonNull X'", list);
+ expectedInfo=
+ "<dl><dt>class pack/age/X</dt>" +
+ "<dd><X:Ljava/lang/Object;T::Ljava/util/List<TX;>;></dd>" +
+ "<dd><<b>1</b>X:Ljava/lang/Object;0T::Ljava/util/List<TX;>;></dd>" + // <= 1
+ "</dl>";
+ assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+ proposal= findProposalByName("Annotate as '@Nullable X'", list);
+ expectedInfo=
+ "<dl><dt>class pack/age/X</dt>" +
+ "<dd><X:Ljava/lang/Object;T::Ljava/util/List<TX;>;></dd>" +
+ "<dd><<b>0</b>X:Ljava/lang/Object;0T::Ljava/util/List<TX;>;></dd>" + // <= 0
+ "</dl>";
+ assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+ document= javaEditor.getDocumentProvider().getDocument(javaEditor.getEditorInput());
+ proposal.apply(document);
+
+ annotationFile= fJProject1.getProject().getFile(new Path(ANNOTATION_PATH).append(X_PATH+".eea"));
+ assertTrue("Annotation file should have been created", annotationFile.exists());
+
+ expectedContent=
+ "class pack/age/X\n" +
+ " <X:Ljava/lang/Object;T::Ljava/util/List<TX;>;>\n" +
+ " <0X:Ljava/lang/Object;0T::Ljava/util/List<TX;>;>\n";
checkContentOfFile("annotation file content", annotationFile, expectedContent);
} finally {
JavaPlugin.getActivePage().closeAllEditors(false);
diff --git a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/ExternalNullAnnotationChangeProposals.java b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/ExternalNullAnnotationChangeProposals.java
index f86f4b7..92739ef 100644
--- a/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/ExternalNullAnnotationChangeProposals.java
+++ b/org.eclipse.jdt.ui/core extension/org/eclipse/jdt/internal/corext/fix/ExternalNullAnnotationChangeProposals.java
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (c) 2015, 2017 GK Software AG and others.
+ * Copyright (c) 2015, 2019 GK Software AG and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
@@ -19,8 +19,10 @@
import static org.eclipse.jdt.core.util.ExternalAnnotationUtil.annotateMember;
import static org.eclipse.jdt.core.util.ExternalAnnotationUtil.annotateMethodParameterType;
import static org.eclipse.jdt.core.util.ExternalAnnotationUtil.annotateMethodReturnType;
+import static org.eclipse.jdt.core.util.ExternalAnnotationUtil.annotateMethodTypeParameter;
import static org.eclipse.jdt.core.util.ExternalAnnotationUtil.extractGenericSignature;
import static org.eclipse.jdt.core.util.ExternalAnnotationUtil.extractGenericTypeSignature;
+import static org.eclipse.jdt.core.util.ExternalAnnotationUtil.extractGenericTypeParametersSignature;
import static org.eclipse.jdt.core.util.ExternalAnnotationUtil.getAnnotationFile;
import static org.eclipse.jdt.internal.ui.text.spelling.WordCorrectionProposal.getHtmlRepresentation;
@@ -70,6 +72,7 @@
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.StructuralPropertyDescriptor;
import org.eclipse.jdt.core.dom.Type;
+import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.TypeParameter;
import org.eclipse.jdt.core.dom.VariableDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
@@ -205,7 +208,10 @@
public String getAdditionalProposalInfo() {
StringBuilder buffer= new StringBuilder();
buffer.append("<dl>"); //$NON-NLS-1$
- buffer.append("<dt>").append(getHtmlRepresentation(fSelector)).append("</dt>"); //$NON-NLS-1$ //$NON-NLS-2$
+ if (fSelector != null)
+ buffer.append("<dt>").append(getHtmlRepresentation(fSelector)).append("</dt>"); //$NON-NLS-1$ //$NON-NLS-2$
+ else
+ buffer.append("<dt>class ").append(getHtmlRepresentation(fAffectedTypeName)).append("</dt>"); //$NON-NLS-1$ //$NON-NLS-2$
buffer.append("<dd>").append(getHtmlRepresentation(fSignature)).append("</dd>"); //$NON-NLS-1$ //$NON-NLS-2$
buffer.append("<dd>").append(getFullAnnotatedSignatureHTML()).append("</dd>"); //$NON-NLS-1$ //$NON-NLS-2$
buffer.append("</dl>"); //$NON-NLS-1$
@@ -295,6 +301,25 @@
}
}
+ static class TypeParameterAnnotationRewriteProposal extends SignatureAnnotationChangeProposal {
+
+ int fParamIdx;
+
+ TypeParameterAnnotationRewriteProposal(int paramIdx) {
+ fParamIdx= paramIdx;
+ }
+
+ @Override
+ protected void dryRun() {
+ fDryRun= ExternalAnnotationUtil.annotateTypeParameter(fCurrentAnnotated, fAnnotatedSignature, fParamIdx, fMergeStrategy);
+ }
+
+ @Override
+ protected void doAnnotateMember(IProgressMonitor monitor) throws CoreException, IOException {
+ annotateMethodTypeParameter(fAffectedTypeName, fAnnotationFile, fSelector, fSignature, fAnnotatedSignature, fParamIdx, fMergeStrategy, monitor);
+ }
+ }
+
static class MissingBindingException extends RuntimeException {
private static final long serialVersionUID= 1L;
ASTNode fNode;
@@ -346,6 +371,12 @@
return binding;
}
+ static ITypeBinding resolveBinding(TypeDeclaration type) {
+ ITypeBinding binding= type.resolveBinding();
+ if (binding == null || binding.isRecovered()) throw new MissingBindingException(type);
+ return binding;
+ }
+
/* Quick assist on class file, propose changes on any type detail. */
public static void collectExternalAnnotationProposals(ICompilationUnit cu, ASTNode coveringNode, int offset, ArrayList<IJavaCompletionProposal> resultingCollection) {
@@ -454,16 +485,11 @@
if (typeBinding.isPrimitive())
return;
}
- outer.accept(rendererNonNull);
- outer.accept(rendererNullable);
- outer.accept(rendererRemove);
- } else { // type parameter
- List<?> siblingList= (List<?>) outer.getParent().getStructuralProperty(outer.getLocationInParent());
- rendererNonNull.visitTypeParameters(siblingList);
- rendererNullable.visitTypeParameters(siblingList);
- rendererRemove.visitTypeParameters(siblingList);
}
-
+ outer.accept(rendererNonNull);
+ outer.accept(rendererNullable);
+ outer.accept(rendererRemove);
+
StructuralPropertyDescriptor locationInParent= outer.getLocationInParent();
ProposalCreator creator= null;
if (locationInParent == MethodDeclaration.RETURN_TYPE2_PROPERTY) {
@@ -483,6 +509,12 @@
VariableDeclarationFragment fragment= (VariableDeclarationFragment) field.fragments().get(0);
creator= new FieldProposalCreator(cu, resolveBinding(fragment));
}
+ } else if (locationInParent == MethodDeclaration.TYPE_PARAMETERS_PROPERTY) {
+ MethodDeclaration method= ASTNodes.getParent(coveringNode, MethodDeclaration.class);
+ creator= new TypeParameterProposalCreator(cu, resolveBinding(method), method.typeParameters().indexOf(outer));
+ } else if (locationInParent == TypeDeclaration.TYPE_PARAMETERS_PROPERTY) {
+ TypeDeclaration type= ASTNodes.getParent(coveringNode, TypeDeclaration.class);
+ creator= new TypeParameterProposalCreator(cu, resolveBinding(type), type.typeParameters().indexOf(outer));
}
if (creator != null) {
createProposalsForType(cu, inner, extraDims, outerExtraDims, annotateVarargs, offset,
@@ -494,8 +526,8 @@
}
}
- static boolean hasAnnotationPathInWorkspace(IJavaProject javaProject, ICompilationUnit cu) {
- IPackageFragmentRoot root= (IPackageFragmentRoot) cu.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT);
+ public static boolean hasAnnotationPathInWorkspace(IJavaProject javaProject, IJavaElement element) {
+ IPackageFragmentRoot root= (IPackageFragmentRoot) element.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT);
if (root != null) {
try {
IClasspathEntry resolvedClasspathEntry= root.getResolvedClasspathEntry();
@@ -585,6 +617,29 @@
}
}
+ private static class TypeParameterProposalCreator extends ProposalCreator {
+ int fParamIdx;
+
+ TypeParameterProposalCreator(ICompilationUnit cu, IMethodBinding methodBinding, int paramIdx) {
+ super(cu, methodBinding.getDeclaringClass(),
+ methodBinding.isConstructor() ? CONSTRUCTOR_SELECTOR : methodBinding.getName(),
+ extractGenericSignature(methodBinding));
+ fParamIdx= paramIdx;
+ }
+
+ TypeParameterProposalCreator(ICompilationUnit cu, ITypeBinding typeBinding, int paramIdx) {
+ super(cu, typeBinding,
+ null,
+ extractGenericTypeParametersSignature(typeBinding));
+ fParamIdx= paramIdx;
+ }
+
+ @Override
+ SignatureAnnotationChangeProposal doCreate(String annotatedSignature, String label) {
+ return new TypeParameterAnnotationRewriteProposal(fParamIdx);
+ }
+ }
+
/* Create one proposal from each of the three given renderers. */
static void createProposalsForType(ICompilationUnit cu, ASTNode type, int dims,
int outerDims, boolean annotateVarargs, int offset,
@@ -772,12 +827,11 @@
break;
}
}
+ boolean boundAdded= false;
+ fBuffer.append(':');
if (classBound != null) {
- fBuffer.append(':');
classBound.accept(this);
- } else {
- ITypeBinding typeBinding= resolveBinding(parameter);
- fBuffer.append(":L").append(binaryName(typeBinding.getSuperclass())).append(';'); //$NON-NLS-1$
+ boundAdded= true;
}
for (Object bound : parameter.typeBounds()) {
if (bound == classBound)
@@ -785,6 +839,10 @@
Type typeBound= (Type) bound;
fBuffer.append(':');
typeBound.accept(this);
+ boundAdded= true;
+ }
+ if (!boundAdded) {
+ fBuffer.append("Ljava/lang/Object;"); //$NON-NLS-1$
}
return false;
}
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/ClassFileEditor.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/ClassFileEditor.java
index 10c04c4..e8adcdf 100644
--- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/ClassFileEditor.java
+++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/javaeditor/ClassFileEditor.java
@@ -92,6 +92,7 @@
import org.eclipse.jdt.core.util.ClassFormatException;
import org.eclipse.jdt.internal.core.manipulation.util.BasicElementLabels;
+import org.eclipse.jdt.internal.corext.fix.ExternalNullAnnotationChangeProposals;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
import org.eclipse.jdt.internal.corext.util.Messages;
@@ -884,7 +885,8 @@
IJavaProject javaProject= file.getJavaProject();
boolean useExternalAnnotations= javaProject != null
- && javaProject.getOption(JavaCore.COMPILER_ANNOTATION_NULL_ANALYSIS, true).equals(JavaCore.ENABLED);
+ && javaProject.getOption(JavaCore.COMPILER_ANNOTATION_NULL_ANALYSIS, true).equals(JavaCore.ENABLED)
+ && ExternalNullAnnotationChangeProposals.hasAnnotationPathInWorkspace(javaProject, file);
annotateAction.setEnabled(useExternalAnnotations);
}