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>&lt;X:Ljava/lang/Object;T:Ljava/util/List&lt;TX;&gt;&gt;(TT;)TX;</dd>" +
-					"<dd>&lt;X:Ljava/lang/Object;1T:Ljava/util/List&lt;TX;&gt;&gt;(TT;)TX;</dd>" + // <= 1
+					"<dd>&lt;X:Ljava/lang/Object;T::Ljava/util/List&lt;TX;&gt;;&gt;(TT;)TX;</dd>" +
+					"<dd>&lt;X:Ljava/lang/Object;<b>1</b>T::Ljava/util/List&lt;TX;&gt;;&gt;(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>&lt;X:Ljava/lang/Object;T:Ljava/util/List&lt;TX;&gt;&gt;(TT;)TX;</dd>" +
-					"<dd>&lt;X:Ljava/lang/Object;0T:Ljava/util/List&lt;TX;&gt;&gt;(TT;)TX;</dd>" + // <= 0
+					"<dd>&lt;X:Ljava/lang/Object;T::Ljava/util/List&lt;TX;&gt;;&gt;(TT;)TX;</dd>" +
+					"<dd>&lt;X:Ljava/lang/Object;<b>0</b>T::Ljava/util/List&lt;TX;&gt;;&gt;(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>&lt;X:Ljava/lang/Object;T::Ljava/util/List&lt;TX;&gt;;&gt;(TT;)TX;</dd>" +
+					"<dd>&lt;<b>1</b>X:Ljava/lang/Object;0T::Ljava/util/List&lt;TX;&gt;;&gt;(TT;)TX;</dd>" + // <= 1
+					"</dl>";
+			assertEquals("expect detail", expectedInfo, proposal.getAdditionalProposalInfo());
+
+			proposal= findProposalByName("Annotate as '@Nullable X'", list);
+			expectedInfo=
+					"<dl><dt>test</dt>" +
+					"<dd>&lt;X:Ljava/lang/Object;T::Ljava/util/List&lt;TX;&gt;;&gt;(TT;)TX;</dd>" +
+					"<dd>&lt;<b>0</b>X:Ljava/lang/Object;0T::Ljava/util/List&lt;TX;&gt;;&gt;(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>&lt;X:Ljava/lang/Object;T::Ljava/util/List&lt;TX;&gt;;&gt;</dd>" +
+					"<dd>&lt;X:Ljava/lang/Object;<b>1</b>T::Ljava/util/List&lt;TX;&gt;;&gt;</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>&lt;X:Ljava/lang/Object;T::Ljava/util/List&lt;TX;&gt;;&gt;</dd>" +
+					"<dd>&lt;X:Ljava/lang/Object;<b>0</b>T::Ljava/util/List&lt;TX;&gt;;&gt;</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>&lt;X:Ljava/lang/Object;T::Ljava/util/List&lt;TX;&gt;;&gt;</dd>" +
+					"<dd>&lt;<b>1</b>X:Ljava/lang/Object;0T::Ljava/util/List&lt;TX;&gt;;&gt;</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>&lt;X:Ljava/lang/Object;T::Ljava/util/List&lt;TX;&gt;;&gt;</dd>" +
+					"<dd>&lt;<b>0</b>X:Ljava/lang/Object;0T::Ljava/util/List&lt;TX;&gt;;&gt;</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);
 
 		}