| author | Stephan Herrmann | 2012-04-29 08:58:23 (EDT) |
|---|---|---|
| committer | Markus Keller | 2012-04-29 08:58:23 (EDT) |
| commit | f63a426a0aa77b391bf2116dcd4eb3749abad092 (patch) (side-by-side diff) | |
| tree | 3c6d62bae4d55c4db93a1a4ece83de22e36c87c6 | |
| parent | 1268e822c591927d3bf64956a8e483c4a1499f6b (diff) | |
| download | eclipse.jdt.ui-f63a426a0aa77b391bf2116dcd4eb3749abad092.zip eclipse.jdt.ui-f63a426a0aa77b391bf2116dcd4eb3749abad092.tar.gz eclipse.jdt.ui-f63a426a0aa77b391bf2116dcd4eb3749abad092.tar.bz2 | |
Bug 337977: [quick fix] Add quickfixes for null annotations
Patch from https://bugs.eclipse.org/bugs/show_bug.cgi?id=337977#c6 integrated into existing QuickFixProcessor
Implementation not thoroughly reviewed!
6 files changed, 1048 insertions, 0 deletions
diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullAnnotationsCleanUp.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullAnnotationsCleanUp.java new file mode 100644 index 0000000..d23ba59 --- a/dev/null +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullAnnotationsCleanUp.java @@ -0,0 +1,157 @@ +/******************************************************************************* + * Copyright (c) 2011, 2012 GK Software AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Stephan Herrmann <stephan@cs.tu-berlin.de> - [quick fix] Add quick fixes for null annotations - https://bugs.eclipse.org/337977 + * IBM Corporation - bug fixes + *******************************************************************************/ +package org.eclipse.jdt.internal.ui.fix; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +import org.eclipse.core.runtime.CoreException; + +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.compiler.IProblem; +import org.eclipse.jdt.core.dom.CompilationUnit; + +import org.eclipse.jdt.ui.cleanup.CleanUpRequirements; +import org.eclipse.jdt.ui.cleanup.ICleanUpFix; +import org.eclipse.jdt.ui.text.java.IProblemLocation; + +/** + * Cleanup for adding required null annotations. + * + * Crafted after the lead of Java50CleanUp + */ +public class NullAnnotationsCleanUp extends AbstractMultiFix { + + private int handledProblemID; + + public NullAnnotationsCleanUp(Map<String, String> options, int handledProblemID) { + super(options); + this.handledProblemID = handledProblemID; + } + + /** + * {@inheritDoc} + */ + @Override + public CleanUpRequirements getRequirements() { + Map<String, String> requiredOptions= getRequiredOptions(); + return new CleanUpRequirements(true, false, false, requiredOptions); + } + + + /** + * {@inheritDoc} + */ + @Override + protected ICleanUpFix createFix(CompilationUnit compilationUnit) throws CoreException { + return this.createFix(compilationUnit, null); + } + + /** + * {@inheritDoc} + */ + @Override + protected ICleanUpFix createFix(CompilationUnit compilationUnit, IProblemLocation[] problems) throws CoreException { + if (compilationUnit == null) + return null; + IProblemLocation[] locations = null; + ArrayList<IProblemLocation> filteredLocations = new ArrayList<IProblemLocation>(); + if (problems != null) { + for (int i = 0; i < problems.length; i++) { + if (problems[i].getProblemId() == this.handledProblemID) + filteredLocations.add(problems[i]); + } + locations = filteredLocations.toArray(new IProblemLocation[filteredLocations.size()]); + } + return NullQuickFixes.createCleanUp(compilationUnit, locations, this.handledProblemID); + } + + private Map<String, String> getRequiredOptions() { + Map<String, String> result= new Hashtable<String, String>(); + // TODO(SH): might set depending on this.handledProblemID, not sure about the benefit + result.put(JavaCore.COMPILER_PB_NULL_SPECIFICATION_VIOLATION, JavaCore.WARNING); + result.put(JavaCore.COMPILER_PB_REDUNDANT_NULL_CHECK, JavaCore.WARNING); + return result; + } + + /** + * {@inheritDoc} + */ + @Override + public String[] getStepDescriptions() { + List<String> result= new ArrayList<String>(); + switch (this.handledProblemID) { + case IProblem.NonNullLocalVariableComparisonYieldsFalse: + case IProblem.RedundantNullCheckOnNonNullLocalVariable: + case IProblem.RequiredNonNullButProvidedNull: + case IProblem.RequiredNonNullButProvidedPotentialNull: + case IProblem.RequiredNonNullButProvidedUnknown: + case IProblem.ParameterLackingNullableAnnotation: + result.add(NullFixMessages.NullAnnotationsCleanUp_add_nullable_annotation); + break; + case IProblem.IllegalDefinitionToNonNullParameter: + case IProblem.IllegalRedefinitionToNonNullParameter: + case IProblem.ParameterLackingNonNullAnnotation: + result.add(NullFixMessages.NullAnnotationsCleanUp_add_nonnull_annotation); + break; + } + return result.toArray(new String[result.size()]); + } + + /** + * {@inheritDoc} + */ + @Override + public String getPreview() { + // not used when not provided as a true cleanup(?) + return "No preview available"; //$NON-NLS-1$ + } + + /** + * {@inheritDoc} + */ + public boolean canFix(ICompilationUnit compilationUnit, IProblemLocation problem) { + int id= problem.getProblemId(); + + if (id == this.handledProblemID) { + // FIXME search specifically: return param (which??) +// if (QuickFixes.hasExplicitNullnessAnnotation(compilationUnit, problem.getOffset())) +// return false; + return true; + } + + return false; + } + + + /** + * {@inheritDoc} + */ + @Override + public int computeNumberOfFixes(CompilationUnit compilationUnit) { + int result= 0; + + IProblem[] problems= compilationUnit.getProblems(); + for (int i= 0; i < problems.length; i++) { + int id= problems[i].getID(); + if (id == this.handledProblemID) { + // FIXME search specifically: return param (which??) +// if (!QuickFixes.hasExplicitNullnessAnnotation(compilationUnit, problems[i].getSourceStart())) + result++; + } + } + return result; + } +} diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullFixMessages.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullFixMessages.java new file mode 100644 index 0000000..3fd05d4 --- a/dev/null +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullFixMessages.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2011, 2012 GK Software AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Stephan Herrmann <stephan@cs.tu-berlin.de> - [quick fix] Add quick fixes for null annotations - https://bugs.eclipse.org/337977 + * IBM Corporation - bug fixes + *******************************************************************************/ +package org.eclipse.jdt.internal.ui.fix; + +import org.eclipse.osgi.util.NLS; + +public class NullFixMessages extends NLS { + private static final String BUNDLE_NAME = "org.eclipse.jdt.internal.ui.fix.NullFixMessages"; //$NON-NLS-1$ + + + public static String NullAnnotationsCleanUp_add_nullable_annotation; + public static String NullAnnotationsCleanUp_add_nonnull_annotation; + + public static String QuickFixes_add_annotation_change_name; + public static String QuickFixes_declare_method_parameter_nullness; + public static String QuickFixes_declare_method_return_nullness; + public static String QuickFixes_declare_overridden_parameter_as_nonnull; + public static String QuickFixes_declare_overridden_return_as_nullable; + + static { + // initialize resource bundle + NLS.initializeMessages(BUNDLE_NAME, NullFixMessages.class); + } + + private NullFixMessages() { + } +} diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullFixMessages.properties b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullFixMessages.properties new file mode 100644 index 0000000..172c9cb --- a/dev/null +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullFixMessages.properties @@ -0,0 +1,18 @@ +############################################################################### +# Copyright (c) 2011, 2012 GK Software AG and others. +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +# +# Contributors: +# Stephan Herrmann <stephan@cs.tu-berlin.de> - [quick fix] Add quick fixes for null annotations - https://bugs.eclipse.org/337977 +# IBM Corporation - bug fixes +############################################################################### +NullAnnotationsCleanUp_add_nullable_annotation=Add missing @Nullable annotation +NullAnnotationsCleanUp_add_nonnull_annotation=Add missing @NonNull annotation +QuickFixes_add_annotation_change_name=Add Annotations +QuickFixes_declare_method_parameter_nullness=Declare method parameter as @{0} +QuickFixes_declare_method_return_nullness=Declare method return as @{0} +QuickFixes_declare_overridden_parameter_as_nonnull=Adjust overridden method from {1}, mark parameter as @{0} +QuickFixes_declare_overridden_return_as_nullable=Adjust overridden method from {1}, mark as returning @{0} diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullQuickFixes.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullQuickFixes.java new file mode 100644 index 0000000..610a138 --- a/dev/null +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullQuickFixes.java @@ -0,0 +1,270 @@ +/******************************************************************************* + * Copyright (c) 2011, 2012 GK Software AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Stephan Herrmann <stephan@cs.tu-berlin.de> - [quick fix] Add quick fixes for null annotations - https://bugs.eclipse.org/337977 + * IBM Corporation - bug fixes + *******************************************************************************/ +package org.eclipse.jdt.internal.ui.fix; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.swt.graphics.Image; + +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.compiler.IProblem; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.IBinding; +import org.eclipse.jdt.core.dom.IVariableBinding; +import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.VariableDeclaration; + +import org.eclipse.jdt.internal.corext.dom.ASTNodes; +import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFix; +import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFix.CompilationUnitRewriteOperation; +import org.eclipse.jdt.internal.corext.util.JavaModelUtil; + +import org.eclipse.jdt.ui.cleanup.ICleanUpFix; +import org.eclipse.jdt.ui.text.java.IInvocationContext; +import org.eclipse.jdt.ui.text.java.IProblemLocation; +import org.eclipse.jdt.ui.text.java.correction.ICommandAccess; + +import org.eclipse.jdt.internal.ui.JavaPluginImages; +import org.eclipse.jdt.internal.ui.text.correction.ProblemLocation; +import org.eclipse.jdt.internal.ui.text.correction.proposals.FixCorrectionProposal; + +/** + * Quick Fixes for null-annotation related problems. + */ +public class NullQuickFixes { + + /** Small adaptation just to make available the 'compilationUnit' passed at instantiation time. */ + private static class MyCURewriteOperationsFix extends CompilationUnitRewriteOperationsFix { + CompilationUnit cu; + public MyCURewriteOperationsFix(String name, CompilationUnit compilationUnit, CompilationUnitRewriteOperation[] operations) { + super(name, compilationUnit, operations); + this.cu = compilationUnit; + } + } + + public static void addReturnAndArgumentTypeProposal(IInvocationContext context, IProblemLocation problem, Collection<ICommandAccess> proposals) { + CompilationUnit astRoot= context.getASTRoot(); + ASTNode selectedNode= problem.getCoveringNode(astRoot); + + if (isComplainingAboutArgument(selectedNode) || isComplainingAboutReturn(selectedNode)) + addNullAnnotationInSignatureProposal(context, problem, proposals, false); + } + + public static void addNullAnnotationInSignatureProposal(IInvocationContext context, + IProblemLocation problem, + Collection<ICommandAccess> proposals, + boolean modifyOverridden) { + MyCURewriteOperationsFix fix= createNullAnnotationInSignatureFix(context.getASTRoot(), problem, modifyOverridden); + + if (fix != null) { + Image image= JavaPluginImages.get(JavaPluginImages.IMG_CORRECTION_CHANGE); + Map<String, String> options= new Hashtable<String, String>(); + if (fix.cu != context.getASTRoot()) { + // workaround: adjust the unit to operate on, depending on the findings of RewriteOperations.createAddAnnotationOperation(..) + final CompilationUnit cu = fix.cu; + final IInvocationContext originalContext = context; + context = new IInvocationContext() { + public int getSelectionOffset() { + return originalContext.getSelectionOffset(); + } + public int getSelectionLength() { + return originalContext.getSelectionLength(); + } + public ASTNode getCoveringNode() { + return originalContext.getCoveringNode(); + } + public ASTNode getCoveredNode() { + return originalContext.getCoveredNode(); + } + public ICompilationUnit getCompilationUnit() { + return (ICompilationUnit) cu.getJavaElement(); + } + + public CompilationUnit getASTRoot() { + return cu; + } + }; + } + int relevance = modifyOverridden ? 15 : 20; // TODO(SH): insert suitable values, just raise local change above change in overridden method + FixCorrectionProposal proposal= new FixCorrectionProposal(fix, new NullAnnotationsCleanUp(options, problem.getProblemId()), relevance, image, context); + proposals.add(proposal); + } + } + + private static boolean isComplainingAboutArgument(ASTNode selectedNode) { + if (!(selectedNode instanceof SimpleName)) { + return false; + } + SimpleName nameNode= (SimpleName) selectedNode; + IBinding binding = nameNode.resolveBinding(); + if (binding.getKind() == IBinding.VARIABLE && ((IVariableBinding) binding).isParameter()) + return true; + VariableDeclaration argDecl = (VariableDeclaration) ASTNodes.getParent(selectedNode, VariableDeclaration.class); + if (argDecl != null) + binding = argDecl.resolveBinding(); + if (binding.getKind() == IBinding.VARIABLE && ((IVariableBinding) binding).isParameter()) + return true; + return false; + } + + private static boolean isComplainingAboutReturn(ASTNode selectedNode) { + return selectedNode.getParent().getNodeType() == ASTNode.RETURN_STATEMENT; + } + + static MyCURewriteOperationsFix createNullAnnotationInSignatureFix(CompilationUnit compilationUnit, IProblemLocation problem, boolean modifyOverridden) { + String nullableAnnotationName = getNullableAnnotationName(compilationUnit.getJavaElement(), false); + String nonNullAnnotationName = getNonNullAnnotationName(compilationUnit.getJavaElement(), false); + String annotationToAdd = nullableAnnotationName; + String annotationToRemove = nonNullAnnotationName; + + switch (problem.getProblemId()) { + case IProblem.IllegalDefinitionToNonNullParameter: + case IProblem.IllegalRedefinitionToNonNullParameter: + // case ParameterLackingNullableAnnotation: // never proposed with modifyOverridden + if (modifyOverridden) { + annotationToAdd = nonNullAnnotationName; + annotationToRemove = nullableAnnotationName; + } + break; + case IProblem.ParameterLackingNonNullAnnotation: + case IProblem.IllegalReturnNullityRedefinition: + if (!modifyOverridden) { + annotationToAdd = nonNullAnnotationName; + annotationToRemove = nullableAnnotationName; + } + break; + // all others propose to add @Nullable + } + + // when performing one change at a time we can actually modify another CU than the current one: + NullRewriteOperations.SignatureAnnotationRewriteOperation operation = + NullRewriteOperations.createAddAnnotationOperation( + compilationUnit, problem, annotationToAdd, annotationToRemove, null, + false/*thisUnitOnly*/, true/*allowRemove*/, modifyOverridden); + if (operation == null) + return null; + + return new MyCURewriteOperationsFix(operation.getMessage(), + operation.getCompilationUnit(), // note that this uses the findings from createAddAnnotationOperation(..) + new NullRewriteOperations.SignatureAnnotationRewriteOperation[] {operation}); + } + + // Entry for NullAnnotationsCleanup: + public static ICleanUpFix createCleanUp(CompilationUnit compilationUnit, IProblemLocation[] locations, int problemID) { + + ICompilationUnit cu= (ICompilationUnit)compilationUnit.getJavaElement(); + if (!JavaModelUtil.is50OrHigher(cu.getJavaProject())) + return null; + + List<CompilationUnitRewriteOperation> operations= new ArrayList<CompilationUnitRewriteOperation>(); + + if (locations == null) { + org.eclipse.jdt.core.compiler.IProblem[] problems= compilationUnit.getProblems(); + locations= new IProblemLocation[problems.length]; + for (int i= 0; i < problems.length; i++) { + if (problems[i].getID() == problemID) + locations[i]= new ProblemLocation(problems[i]); + } + } + + createAddNullAnnotationOperations(compilationUnit, locations, operations); + + if (operations.size() == 0) + return null; + + CompilationUnitRewriteOperation[] operationsArray= operations.toArray(new CompilationUnitRewriteOperation[operations.size()]); + return new MyCURewriteOperationsFix(NullFixMessages.QuickFixes_add_annotation_change_name, compilationUnit, operationsArray); + } + + private static void createAddNullAnnotationOperations(CompilationUnit compilationUnit, IProblemLocation[] locations, List<CompilationUnitRewriteOperation> result) { + String nullableAnnotationName = getNullableAnnotationName(compilationUnit.getJavaElement(), false); + String nonNullAnnotationName = getNonNullAnnotationName(compilationUnit.getJavaElement(), false); + Set<String> handledPositions = new HashSet<String>(); + for (int i= 0; i < locations.length; i++) { + IProblemLocation problem= locations[i]; + if (problem == null) continue; // problem was filtered out by createCleanUp() + String annotationToAdd = nullableAnnotationName; + String annotationToRemove = nonNullAnnotationName; + switch (problem.getProblemId()) { + case IProblem.IllegalDefinitionToNonNullParameter: + case IProblem.IllegalRedefinitionToNonNullParameter: + case IProblem.ParameterLackingNonNullAnnotation: + case IProblem.IllegalReturnNullityRedefinition: + annotationToAdd = nonNullAnnotationName; + annotationToRemove = nullableAnnotationName; + // all others propose to add @Nullable + } + // when performing multiple changes we can only modify the one CU that the CleanUp infrastructure provides to the operation. + CompilationUnitRewriteOperation fix = NullRewriteOperations.createAddAnnotationOperation( + compilationUnit, problem, annotationToAdd, annotationToRemove, + handledPositions, true/*thisUnitOnly*/, false/*allowRemove*/, false/*modifyOverridden*/); + if (fix != null) + result.add(fix); + } + } + + public static boolean isMissingNullAnnotationProblem(int id) { + return id == IProblem.RequiredNonNullButProvidedNull || id == IProblem.RequiredNonNullButProvidedPotentialNull + || id == IProblem.IllegalReturnNullityRedefinition + || mayIndicateParameterNullcheck(id); + } + + public static boolean mayIndicateParameterNullcheck(int problemId) { + return problemId == IProblem.NonNullLocalVariableComparisonYieldsFalse || problemId == IProblem.RedundantNullCheckOnNonNullLocalVariable; + } + + public static boolean hasExplicitNullAnnotation(ICompilationUnit compilationUnit, int offset) { +// FIXME(SH): check for existing annotations disabled due to lack of precision: +// should distinguish what is actually annotated (return? param? which?) +// try { +// IJavaElement problemElement = compilationUnit.getElementAt(offset); +// if (problemElement.getElementType() == IJavaElement.METHOD) { +// IMethod method = (IMethod) problemElement; +// String nullable = getNullableAnnotationName(compilationUnit, true); +// String nonnull = getNonNullAnnotationName(compilationUnit, true); +// for (IAnnotation annotation : method.getAnnotations()) { +// if ( annotation.getElementName().equals(nonnull) +// || annotation.getElementName().equals(nullable)) +// return true; +// } +// } +// } catch (JavaModelException jme) { +// /* nop */ +// } + return false; + } + + public static String getNullableAnnotationName(IJavaElement javaElement, boolean makeSimple) { + String qualifiedName = javaElement.getJavaProject().getOption(JavaCore.COMPILER_NULLABLE_ANNOTATION_NAME, true); + int lastDot; + if (makeSimple && qualifiedName != null && (lastDot = qualifiedName.lastIndexOf('.')) != -1) + return qualifiedName.substring(lastDot+1); + return qualifiedName; + } + + public static String getNonNullAnnotationName(IJavaElement javaElement, boolean makeSimple) { + String qualifiedName = javaElement.getJavaProject().getOption(JavaCore.COMPILER_NONNULL_ANNOTATION_NAME, true); + int lastDot; + if (makeSimple && qualifiedName != null && (lastDot = qualifiedName.lastIndexOf('.')) != -1) + return qualifiedName.substring(lastDot+1); + return qualifiedName; + } +} diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullRewriteOperations.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullRewriteOperations.java new file mode 100644 index 0000000..5f282c7 --- a/dev/null +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/fix/NullRewriteOperations.java @@ -0,0 +1,541 @@ +/******************************************************************************* + * Copyright (c) 2011, 2012 GK Software AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Stephan Herrmann <stephan@cs.tu-berlin.de> - [quick fix] Add quick fixes for null annotations - https://bugs.eclipse.org/337977 + * IBM Corporation - bug fixes + *******************************************************************************/ +package org.eclipse.jdt.internal.ui.fix; + +import java.util.List; +import java.util.Set; + +import org.eclipse.core.runtime.CoreException; + +import org.eclipse.text.edits.TextEditGroup; + +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.compiler.IProblem; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.BodyDeclaration; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.IBinding; +import org.eclipse.jdt.core.dom.IExtendedModifier; +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.MarkerAnnotation; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.MethodInvocation; +import org.eclipse.jdt.core.dom.Name; +import org.eclipse.jdt.core.dom.SimpleName; +import org.eclipse.jdt.core.dom.SingleVariableDeclaration; +import org.eclipse.jdt.core.dom.VariableDeclaration; +import org.eclipse.jdt.core.dom.rewrite.ImportRewrite; +import org.eclipse.jdt.core.dom.rewrite.ListRewrite; + +import org.eclipse.jdt.internal.corext.dom.ASTNodes; +import org.eclipse.jdt.internal.corext.dom.Bindings; +import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFix.CompilationUnitRewriteOperation; +import org.eclipse.jdt.internal.corext.fix.LinkedProposalModel; +import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite; +import org.eclipse.jdt.internal.corext.util.JavaModelUtil; +import org.eclipse.jdt.internal.corext.util.Messages; + +import org.eclipse.jdt.ui.text.java.IProblemLocation; + +import org.eclipse.jdt.internal.ui.text.correction.ASTResolving; +import org.eclipse.jdt.internal.ui.viewsupport.BasicElementLabels; + +public class NullRewriteOperations { + + static abstract class SignatureAnnotationRewriteOperation extends CompilationUnitRewriteOperation { + String fAnnotationToAdd; + String fAnnotationToRemove; + boolean fAllowRemove; + CompilationUnit fUnit; + + protected String fKey; + protected String fMessage; + + /** A globally unique key that identifies the position being annotated (for avoiding double annotations). */ + public String getKey() { return this.fKey; } + + public CompilationUnit getCompilationUnit() { + return fUnit; + } + + + boolean checkExisting(@SuppressWarnings("rawtypes") List existingModifiers, + ListRewrite listRewrite, + TextEditGroup editGroup) + { + for (Object mod : existingModifiers) { + if (mod instanceof MarkerAnnotation) { + MarkerAnnotation annotation = (MarkerAnnotation) mod; + String existingName = annotation.getTypeName().getFullyQualifiedName(); + int lastDot = fAnnotationToRemove.lastIndexOf('.'); + if ( existingName.equals(fAnnotationToRemove) + || (lastDot != -1 && fAnnotationToRemove.substring(lastDot+1).equals(existingName))) + { + if (!fAllowRemove) + return false; // veto this change + listRewrite.remove(annotation, editGroup); + return true; + } + // paranoia: check if by accident the annotation is already present (shouldn't happen): + lastDot = fAnnotationToAdd.lastIndexOf('.'); + if ( existingName.equals(fAnnotationToAdd) + || (lastDot != -1 && fAnnotationToAdd.substring(lastDot+1).equals(existingName))) + { + return false; // already present + } + } + } + return true; + } + + public String getMessage() { + return fMessage; + } + } + + /** + * Rewrite operation that inserts an annotation into a method signature. + * + * Crafted after the lead of Java50Fix.AnnotationRewriteOperation + */ + static class ReturnAnnotationRewriteOperation extends SignatureAnnotationRewriteOperation { + + private final BodyDeclaration fBodyDeclaration; + + ReturnAnnotationRewriteOperation(CompilationUnit unit, + MethodDeclaration method, + String annotationToAdd, + String annotationToRemove, + boolean allowRemove, + String message) + { + fUnit = unit; + fKey= method.resolveBinding().getKey()+"<return>"; //$NON-NLS-1$ + fBodyDeclaration= method; + fAnnotationToAdd= annotationToAdd; + fAnnotationToRemove= annotationToRemove; + fAllowRemove= allowRemove; + fMessage = message; + } + + @Override + public void rewriteAST(CompilationUnitRewrite cuRewrite, LinkedProposalModel model) throws CoreException { + AST ast= cuRewrite.getRoot().getAST(); + ListRewrite listRewrite= cuRewrite.getASTRewrite().getListRewrite(fBodyDeclaration, fBodyDeclaration.getModifiersProperty()); + TextEditGroup group= createTextEditGroup(fMessage, cuRewrite); + if (!checkExisting(fBodyDeclaration.modifiers(), listRewrite, group)) + return; + Annotation newAnnotation= ast.newMarkerAnnotation(); + ImportRewrite importRewrite = cuRewrite.getImportRewrite(); + String resolvableName = importRewrite.addImport(fAnnotationToAdd); + newAnnotation.setTypeName(ast.newName(resolvableName)); + listRewrite.insertLast(newAnnotation, group); // null annotation is last modifier, directly preceding the return type + } + } + + static class ParameterAnnotationRewriteOperation extends SignatureAnnotationRewriteOperation { + + private SingleVariableDeclaration fArgument; + + ParameterAnnotationRewriteOperation(CompilationUnit unit, + MethodDeclaration method, + String annotationToAdd, + String annotationToRemove, + String paramName, + boolean allowRemove, + String message) + { + fUnit= unit; + fKey= method.resolveBinding().getKey(); + fAnnotationToAdd= annotationToAdd; + fAnnotationToRemove= annotationToRemove; + fAllowRemove= allowRemove; + fMessage = message; + for (Object param : method.parameters()) { + SingleVariableDeclaration argument = (SingleVariableDeclaration) param; + if (argument.getName().getIdentifier().equals(paramName)) { + fArgument= argument; + fKey += argument.getName().getIdentifier(); + return; + } + } + // shouldn't happen, we've checked that paramName indeed denotes a parameter. + throw new RuntimeException("Argument "+paramName+" not found in method "+method.getName().getIdentifier()); //$NON-NLS-1$ //$NON-NLS-2$ + } + + ParameterAnnotationRewriteOperation(CompilationUnit unit, + MethodDeclaration method, + String annotationToAdd, + String annotationToRemove, + int paramIdx, + boolean allowRemove, + String message) + { + fUnit= unit; + fKey= method.resolveBinding().getKey(); + fAnnotationToAdd= annotationToAdd; + fAnnotationToRemove= annotationToRemove; + fAllowRemove= allowRemove; + fArgument = (SingleVariableDeclaration) method.parameters().get(paramIdx); + fKey += fArgument.getName().getIdentifier(); + fMessage = message; + } + + @Override + public void rewriteAST(CompilationUnitRewrite cuRewrite, LinkedProposalModel linkedModel) throws CoreException { + AST ast= cuRewrite.getRoot().getAST(); + ListRewrite listRewrite= cuRewrite.getASTRewrite().getListRewrite(fArgument, SingleVariableDeclaration.MODIFIERS2_PROPERTY); + TextEditGroup group= createTextEditGroup(fMessage, cuRewrite); + if (!checkExisting(fArgument.modifiers(), listRewrite, group)) + return; + Annotation newAnnotation= ast.newMarkerAnnotation(); + ImportRewrite importRewrite = cuRewrite.getImportRewrite(); + String resolvableName = importRewrite.addImport(fAnnotationToAdd); + newAnnotation.setTypeName(ast.newName(resolvableName)); + listRewrite.insertLast(newAnnotation, group); // null annotation is last modifier, directly preceding the type + } + } + + // Entry for QuickFixes: + public static SignatureAnnotationRewriteOperation createAddAnnotationOperation(CompilationUnit compilationUnit, + IProblemLocation problem, + String annotationToAdd, + String annotationToRemove, + Set<String> handledPositions, + boolean thisUnitOnly, + boolean allowRemove, + boolean modifyOverridden) + { + SignatureAnnotationRewriteOperation result = modifyOverridden + ? createAddAnnotationToOverriddenOperation(compilationUnit, problem, annotationToAdd, annotationToRemove, + handledPositions, thisUnitOnly, allowRemove) + : createAddAnnotationOperation(compilationUnit, problem, annotationToAdd, annotationToRemove, + handledPositions, thisUnitOnly, allowRemove); + if (handledPositions != null && result != null) { + if (handledPositions.contains(result.getKey())) + return null; + handledPositions.add(result.getKey()); + } + return result; + } + static SignatureAnnotationRewriteOperation createAddAnnotationOperation(CompilationUnit compilationUnit, + IProblemLocation problem, + String annotationToAdd, + String annotationToRemove, + Set<String> handledPositions, + boolean thisUnitOnly, + boolean allowRemove) + { + ICompilationUnit cu= (ICompilationUnit)compilationUnit.getJavaElement(); + if (!JavaModelUtil.is50OrHigher(cu.getJavaProject())) + return null; + + ASTNode selectedNode= problem.getCoveringNode(compilationUnit); + if (selectedNode == null) + return null; + + ASTNode declaringNode= getDeclaringNode(selectedNode); + + switch (problem.getProblemId()) { + case IProblem.IllegalDefinitionToNonNullParameter: +// case IllegalRedefinitionToNonNullParameter: + // these affect another method + break; + case IProblem.IllegalReturnNullityRedefinition: + if (declaringNode == null) + declaringNode = selectedNode; + break; // do propose changes even if we already have an annotation + default: + // if this method has annotations, don't change'em + if (NullQuickFixes.hasExplicitNullAnnotation(cu, problem.getOffset())) + return null; + } + + SignatureAnnotationRewriteOperation result = null; + String message = null; + String annotationNameLabel = annotationToAdd; + int lastDot = annotationToAdd.lastIndexOf('.'); + if (lastDot != -1) + annotationNameLabel = annotationToAdd.substring(lastDot+1); + annotationNameLabel = BasicElementLabels.getJavaElementName(annotationNameLabel); + + if (selectedNode.getParent() instanceof MethodInvocation) { + // DefiniteNullToNonNullParameter || PotentialNullToNonNullParameter + MethodInvocation methodInvocation = (MethodInvocation)selectedNode.getParent(); + int paramIdx = methodInvocation.arguments().indexOf(selectedNode); + IMethodBinding methodBinding = methodInvocation.resolveMethodBinding(); + compilationUnit = findCUForMethod(compilationUnit, cu, methodBinding); + if (compilationUnit == null) + return null; + if (thisUnitOnly && !compilationUnit.getJavaElement().equals(cu)) + return null; + ASTNode methodDecl = compilationUnit.findDeclaringNode(methodBinding.getKey()); + if (methodDecl == null) + return null; + message = Messages.format(NullFixMessages.QuickFixes_declare_method_parameter_nullness, annotationNameLabel); + result = new ParameterAnnotationRewriteOperation(compilationUnit, + (MethodDeclaration) methodDecl, + annotationToAdd, + annotationToRemove, + paramIdx, + allowRemove, + message); + } else if (declaringNode instanceof MethodDeclaration) { + + // complaint is in signature of this method + + MethodDeclaration declaration= (MethodDeclaration) declaringNode; + + switch (problem.getProblemId()) { + case IProblem.ParameterLackingNonNullAnnotation: + case IProblem.ParameterLackingNullableAnnotation: + case IProblem.IllegalDefinitionToNonNullParameter: + case IProblem.IllegalRedefinitionToNonNullParameter: + case IProblem.NonNullLocalVariableComparisonYieldsFalse: + case IProblem.RedundantNullCheckOnNonNullLocalVariable: + // statements suggest changing parameters: + if (declaration.getNodeType() == ASTNode.METHOD_DECLARATION) { + String paramName = findAffectedParameterName(selectedNode); + if (paramName != null) { + message = Messages.format(NullFixMessages.QuickFixes_declare_method_parameter_nullness, annotationNameLabel); + result = new ParameterAnnotationRewriteOperation(compilationUnit, + declaration, + annotationToAdd, + annotationToRemove, + paramName, + allowRemove, + message); + } + } + break; + case IProblem.IllegalReturnNullityRedefinition: + case IProblem.RequiredNonNullButProvidedNull: + case IProblem.RequiredNonNullButProvidedPotentialNull: + case IProblem.RequiredNonNullButProvidedUnknown: + message = Messages.format(NullFixMessages.QuickFixes_declare_method_return_nullness, annotationNameLabel); + result = new ReturnAnnotationRewriteOperation(compilationUnit, + declaration, + annotationToAdd, + annotationToRemove, + allowRemove, + message); + break; + } + + } + return result; + } + static SignatureAnnotationRewriteOperation createAddAnnotationToOverriddenOperation(CompilationUnit compilationUnit, + IProblemLocation problem, + String annotationToAdd, + String annotationToRemove, + Set<String> handledPositions, + boolean thisUnitOnly, + boolean allowRemove) + { + ICompilationUnit cu = (ICompilationUnit) compilationUnit.getJavaElement(); + if (!JavaModelUtil.is50OrHigher(cu.getJavaProject())) + return null; + + ASTNode selectedNode = problem.getCoveringNode(compilationUnit); + if (selectedNode == null) + return null; + + ASTNode declaringNode = getDeclaringNode(selectedNode); + + switch (problem.getProblemId()) { + case IProblem.IllegalDefinitionToNonNullParameter: + case IProblem.IllegalRedefinitionToNonNullParameter: + break; + case IProblem.IllegalReturnNullityRedefinition: + if (declaringNode == null) + declaringNode = selectedNode; + break; + default: + return null; + } + + String annotationNameLabel = annotationToAdd; + int lastDot = annotationToAdd.lastIndexOf('.'); + if (lastDot != -1) + annotationNameLabel = annotationToAdd.substring(lastDot + 1); + annotationNameLabel = BasicElementLabels.getJavaElementName(annotationNameLabel); + + if (declaringNode instanceof MethodDeclaration) { + + // complaint is in signature of this method + + MethodDeclaration declaration = (MethodDeclaration) declaringNode; + + switch (problem.getProblemId()) { + case IProblem.IllegalDefinitionToNonNullParameter: + case IProblem.IllegalRedefinitionToNonNullParameter: + return createChangeOverriddenParameterOperation(compilationUnit, cu, declaration, selectedNode, + allowRemove, annotationToAdd, annotationToRemove, annotationNameLabel); + case IProblem.IllegalReturnNullityRedefinition: + if (hasNullAnnotation(declaration)) { // don't adjust super if local has no explicit annotation (?) + return createChangeOverriddenReturnOperation(compilationUnit, cu, declaration, allowRemove, + annotationToAdd, annotationToRemove, annotationNameLabel); + } + } + + } + return null; + } + + static SignatureAnnotationRewriteOperation createChangeOverriddenParameterOperation(CompilationUnit compilationUnit, + ICompilationUnit cu, + MethodDeclaration declaration, + ASTNode selectedNode, + boolean allowRemove, + String annotationToAdd, + String annotationToRemove, + String annotationNameLabel) + { + SignatureAnnotationRewriteOperation result; + String message; + IMethodBinding methodDeclBinding= declaration.resolveBinding(); + if (methodDeclBinding == null) + return null; + + IMethodBinding overridden= Bindings.findOverriddenMethod(methodDeclBinding, false); + if (overridden == null) + return null; + compilationUnit = findCUForMethod(compilationUnit, cu, overridden); + if (compilationUnit == null) + return null; + ASTNode methodDecl = compilationUnit.findDeclaringNode(overridden.getKey()); + if (methodDecl == null) + return null; + declaration = (MethodDeclaration) methodDecl; + message = Messages.format(NullFixMessages.QuickFixes_declare_overridden_parameter_as_nonnull, + new String[] { + annotationNameLabel, + BasicElementLabels.getJavaElementName(overridden.getDeclaringClass().getName()) + }); + String paramName = findAffectedParameterName(selectedNode); + result = new ParameterAnnotationRewriteOperation(compilationUnit, + declaration, + annotationToAdd, + annotationToRemove, + paramName, + allowRemove, + message); + return result; + } + + static String findAffectedParameterName(ASTNode selectedNode) { + VariableDeclaration argDecl = selectedNode instanceof VariableDeclaration + ? (VariableDeclaration) selectedNode + : (VariableDeclaration) ASTNodes.getParent(selectedNode, VariableDeclaration.class); + if (argDecl != null) + return argDecl.getName().getIdentifier(); + if (selectedNode.getNodeType() == ASTNode.SIMPLE_NAME) { + IBinding binding = ((SimpleName)selectedNode).resolveBinding(); + if (binding.getKind() == IBinding.VARIABLE && ((IVariableBinding)binding).isParameter()) + return ((SimpleName)selectedNode).getIdentifier(); + } + return null; + } + + static boolean hasNullAnnotation(MethodDeclaration decl) { + List<IExtendedModifier> modifiers = decl.modifiers(); + String nonnull = NullQuickFixes.getNonNullAnnotationName(decl.resolveBinding().getJavaElement(), false); + String nullable = NullQuickFixes.getNullableAnnotationName(decl.resolveBinding().getJavaElement(), false); + for (Object mod : modifiers) { + if (mod instanceof Annotation) { + Name annotationName = ((Annotation) mod).getTypeName(); + String fullyQualifiedName = annotationName.getFullyQualifiedName(); + if (annotationName.isSimpleName() + ? nonnull.endsWith(fullyQualifiedName) + : fullyQualifiedName.equals(nonnull)) + return true; + if (annotationName.isSimpleName() + ? nullable.endsWith(fullyQualifiedName) + : fullyQualifiedName.equals(nullable)) + return true; + } + } + return false; + } + + static SignatureAnnotationRewriteOperation createChangeOverriddenReturnOperation(CompilationUnit compilationUnit, + ICompilationUnit cu, + MethodDeclaration declaration, + boolean allowRemove, + String annotationToAdd, + String annotationToRemove, + String annotationNameLabel) + { + SignatureAnnotationRewriteOperation result; + String message; + IMethodBinding methodDeclBinding= declaration.resolveBinding(); + if (methodDeclBinding == null) + return null; + + IMethodBinding overridden= Bindings.findOverriddenMethod(methodDeclBinding, false); + if (overridden == null) + return null; + compilationUnit = findCUForMethod(compilationUnit, cu, overridden); + if (compilationUnit == null) + return null; + ASTNode methodDecl = compilationUnit.findDeclaringNode(overridden.getKey()); + if (methodDecl == null) + return null; + declaration = (MethodDeclaration) methodDecl; +// TODO(SH): decide whether we want to propose overwriting existing annotations in super +// if (hasNullAnnotation(declaration)) // if overridden has explicit declaration don't propose to change it +// return null; + message = Messages.format(NullFixMessages.QuickFixes_declare_overridden_return_as_nullable, + new String[] { + annotationNameLabel, + BasicElementLabels.getJavaElementName(overridden.getDeclaringClass().getName()) + }); + result = new ReturnAnnotationRewriteOperation(compilationUnit, + declaration, + annotationToAdd, + annotationToRemove, + allowRemove, + message); + return result; + } + + static CompilationUnit findCUForMethod(CompilationUnit compilationUnit, ICompilationUnit cu, IMethodBinding methodBinding) + { + ASTNode methodDecl= compilationUnit.findDeclaringNode(methodBinding.getMethodDeclaration()); + if (methodDecl == null) { + // is methodDecl defined in another CU? + ITypeBinding declaringTypeDecl= methodBinding.getDeclaringClass().getTypeDeclaration(); + if (declaringTypeDecl.isFromSource()) { + ICompilationUnit targetCU = null; + try { + targetCU = ASTResolving.findCompilationUnitForBinding(cu, compilationUnit, declaringTypeDecl); + } catch (JavaModelException e) { /* can't do better */ } + if (targetCU != null) { + return ASTResolving.createQuickFixAST(targetCU, null); + } + } + return null; + } + return compilationUnit; + } + + /** The relevant declaring node of a return statement is the enclosing method. */ + static ASTNode getDeclaringNode(ASTNode selectedNode) { + return ASTNodes.getParent(selectedNode, ASTNode.METHOD_DECLARATION); + } +} diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/QuickFixProcessor.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/QuickFixProcessor.java index 4e798af..635ea8f 100644 --- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/QuickFixProcessor.java +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/correction/QuickFixProcessor.java @@ -31,6 +31,7 @@ import org.eclipse.jdt.ui.text.java.IProblemLocation; import org.eclipse.jdt.ui.text.java.IQuickFixProcessor; import org.eclipse.jdt.ui.text.java.correction.ICommandAccess; +import org.eclipse.jdt.internal.ui.fix.NullQuickFixes; import org.eclipse.jdt.internal.ui.text.correction.proposals.ReplaceCorrectionProposal; import org.eclipse.jdt.internal.ui.text.correction.proposals.TaskMarkerProposal; @@ -235,6 +236,16 @@ public class QuickFixProcessor implements IQuickFixProcessor { case IProblem.UnsafeGenericArrayForVarargs: case IProblem.SafeVarargsOnFixedArityMethod : case IProblem.SafeVarargsOnNonFinalInstanceMethod: + case IProblem.RequiredNonNullButProvidedNull: + case IProblem.RequiredNonNullButProvidedPotentialNull: + case IProblem.RequiredNonNullButProvidedUnknown: + case IProblem.IllegalReturnNullityRedefinition: + case IProblem.IllegalRedefinitionToNonNullParameter: + case IProblem.IllegalDefinitionToNonNullParameter: + case IProblem.ParameterLackingNonNullAnnotation: + case IProblem.ParameterLackingNullableAnnotation: + case IProblem.NonNullLocalVariableComparisonYieldsFalse: + case IProblem.RedundantNullCheckOnNonNullLocalVariable: return true; default: return SuppressWarningsSubProcessor.hasSuppressWarningsProposal(cu.getJavaProject(), problemId); @@ -663,6 +674,21 @@ public class QuickFixProcessor implements IQuickFixProcessor { case IProblem.SafeVarargsOnNonFinalInstanceMethod: VarargsWarningsSubProcessor.addRemoveSafeVarargsProposals(context, problem, proposals); break; + case IProblem.IllegalReturnNullityRedefinition: + case IProblem.IllegalDefinitionToNonNullParameter: + case IProblem.IllegalRedefinitionToNonNullParameter: + NullQuickFixes.addNullAnnotationInSignatureProposal(context, problem, proposals, false); + NullQuickFixes.addNullAnnotationInSignatureProposal(context, problem, proposals, true); + break; + case IProblem.RequiredNonNullButProvidedNull: + case IProblem.RequiredNonNullButProvidedPotentialNull: + case IProblem.RequiredNonNullButProvidedUnknown: + case IProblem.ParameterLackingNonNullAnnotation: + case IProblem.ParameterLackingNullableAnnotation: + case IProblem.NonNullLocalVariableComparisonYieldsFalse: + case IProblem.RedundantNullCheckOnNonNullLocalVariable: + NullQuickFixes.addReturnAndArgumentTypeProposal(context, problem, proposals); + break; default: } if (JavaModelUtil.is50OrHigher(context.getCompilationUnit().getJavaProject())) { |

