blob: 267b84f6902a1f9c0bb5c63e3c1e704f2e1f3b4d [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2011, 2017 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 - [quick fix] Add quick fixes for null annotations - https://bugs.eclipse.org/337977
* IBM Corporation - bug fixes
*******************************************************************************/
package org.eclipse.jdt.internal.corext.fix;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
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.MethodDeclaration;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.VariableDeclaration;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.fix.NullAnnotationsRewriteOperations.Builder;
import org.eclipse.jdt.internal.corext.fix.NullAnnotationsRewriteOperations.ChangeKind;
import org.eclipse.jdt.internal.corext.fix.NullAnnotationsRewriteOperations.RemoveRedundantAnnotationRewriteOperation;
import org.eclipse.jdt.internal.corext.fix.NullAnnotationsRewriteOperations.SignatureAnnotationRewriteOperation;
import org.eclipse.jdt.internal.corext.fix.TypeAnnotationRewriteOperations.MoveTypeAnnotationRewriteOperation;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
import org.eclipse.jdt.ui.cleanup.ICleanUpFix;
import org.eclipse.jdt.ui.text.java.IProblemLocation;
import org.eclipse.jdt.internal.ui.text.correction.ProblemLocation;
import org.eclipse.jdt.internal.ui.text.correction.TypeAnnotationSubProcessor;
public class NullAnnotationsFix extends CompilationUnitRewriteOperationsFix {
private CompilationUnit cu;
public NullAnnotationsFix(String name, CompilationUnit compilationUnit, CompilationUnitRewriteOperation[] operations) {
super(name, compilationUnit, operations);
cu= compilationUnit;
}
public CompilationUnit getCu() {
return cu;
}
/* recognizes any simple name referring to a parameter binding */
public 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;
}
/* recognizes the expression of a return statement and the return type of a method declaration. */
public static boolean isComplainingAboutReturn(ASTNode selectedNode) {
if (selectedNode.getParent().getNodeType() == ASTNode.RETURN_STATEMENT)
return true;
while (!(selectedNode instanceof Type)) {
if (selectedNode == null) return false;
selectedNode= selectedNode.getParent();
}
return selectedNode.getLocationInParent() == MethodDeclaration.RETURN_TYPE2_PROPERTY;
}
public static NullAnnotationsFix createNullAnnotationInSignatureFix(CompilationUnit compilationUnit, IProblemLocation problem,
ChangeKind changeKind, boolean isArgumentProblem) {
IJavaElement javaElement= compilationUnit.getJavaElement();
String nullableAnnotationName= getNullableAnnotationName(javaElement, false);
String nonNullAnnotationName= getNonNullAnnotationName(javaElement, false);
Builder builder= new Builder(problem, compilationUnit, nullableAnnotationName, nonNullAnnotationName,
/*allowRemove*/true, isArgumentProblem, changeKind);
boolean addNonNull= false;
switch (problem.getProblemId()) {
case IProblem.IllegalDefinitionToNonNullParameter:
case IProblem.IllegalRedefinitionToNonNullParameter:
// case ParameterLackingNullableAnnotation: // never proposed with modifyOverridden
if (changeKind == ChangeKind.OVERRIDDEN) {
addNonNull= true;
builder.swapAnnotations();
}
break;
case IProblem.ParameterLackingNonNullAnnotation:
case IProblem.IllegalReturnNullityRedefinition:
if (changeKind != ChangeKind.OVERRIDDEN) {
addNonNull= true;
builder.swapAnnotations();
}
break;
case IProblem.RequiredNonNullButProvidedNull:
case IProblem.RequiredNonNullButProvidedPotentialNull:
case IProblem.RequiredNonNullButProvidedUnknown:
case IProblem.RequiredNonNullButProvidedSpecdNullable:
if (isArgumentProblem == (changeKind != ChangeKind.TARGET)) {
addNonNull= true;
builder.swapAnnotations();
}
break;
case IProblem.ConflictingNullAnnotations:
if (isArgumentProblem && changeKind == ChangeKind.INVERSE) {
return null; // cannot redefine @Nullable param to @NonNull
}
//$FALL-THROUGH$
case IProblem.ConflictingInheritedNullAnnotations:
if (changeKind == ChangeKind.INVERSE || changeKind == ChangeKind.OVERRIDDEN) {
addNonNull= true;
builder.swapAnnotations();
}
break;
default:
// all others propose to add @Nullable
}
// when performing one change at a time we can actually modify another CU than the current one:
NullAnnotationsRewriteOperations.SignatureAnnotationRewriteOperation operation= builder.createAddAnnotationOperation(null, false/*thisUnitOnly*/, changeKind);
if (operation == null)
return null;
if (addNonNull) {
operation.fRemoveIfNonNullByDefault= true;
operation.fNonNullByDefaultName= getNonNullByDefaultAnnotationName(javaElement, false);
}
return new NullAnnotationsFix(operation.getMessage(), operation.getCompilationUnit(), // note that this uses the findings from createAddAnnotationOperation(..)
new NullAnnotationsRewriteOperations.SignatureAnnotationRewriteOperation[] { operation });
}
public static NullAnnotationsFix createRemoveRedundantNullAnnotationsFix(CompilationUnit compilationUnit, IProblemLocation problem) {
RemoveRedundantAnnotationRewriteOperation operation= new RemoveRedundantAnnotationRewriteOperation(compilationUnit, problem);
return new NullAnnotationsFix(FixMessages.NullAnnotationsRewriteOperations_remove_redundant_nullness_annotation, compilationUnit, new RemoveRedundantAnnotationRewriteOperation[] { 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<>();
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]);
}
}
String message;
if (TypeAnnotationSubProcessor.hasFixFor(problemID)) {
boolean isMove= createMoveTypeAnnotationOperations(compilationUnit, locations, operations);
message= isMove ? FixMessages.TypeAnnotationFix_move : FixMessages.TypeAnnotationFix_remove;
} else {
createAddNullAnnotationOperations(compilationUnit, locations, operations);
createRemoveRedundantNullAnnotationsOperations(compilationUnit, locations, operations);
message= FixMessages.NullAnnotationsFix_add_annotation_change_name;
}
if (operations.size() == 0)
return null;
CompilationUnitRewriteOperation[] operationsArray= operations.toArray(new CompilationUnitRewriteOperation[operations.size()]);
return new NullAnnotationsFix(message, compilationUnit, operationsArray);
}
private static boolean createMoveTypeAnnotationOperations(CompilationUnit compilationUnit, IProblemLocation[] locations, List<CompilationUnitRewriteOperation> operations) {
boolean isMove= false;
for (IProblemLocation location: locations) {
if (location == null)
continue;
MoveTypeAnnotationRewriteOperation operation= new MoveTypeAnnotationRewriteOperation(compilationUnit, location);
operations.add(operation);
isMove|= operation.isMove();
}
return isMove;
}
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<>();
for (int i= 0; i < locations.length; i++) {
IProblemLocation problem= locations[i];
if (problem == null)
continue; // problem was filtered out by createCleanUp()
boolean isArgumentProblem= isComplainingAboutArgument(problem.getCoveredNode(compilationUnit));
Builder builder= new Builder(problem, compilationUnit, nullableAnnotationName, nonNullAnnotationName,
/*allowRemove*/false, isArgumentProblem, ChangeKind.LOCAL);
boolean addNonNull= false;
// cf. createNullAnnotationInSignatureFix() but changeKind is constantly LOCAL
switch (problem.getProblemId()) {
case IProblem.IllegalDefinitionToNonNullParameter:
case IProblem.IllegalRedefinitionToNonNullParameter:
break;
case IProblem.ParameterLackingNonNullAnnotation:
case IProblem.IllegalReturnNullityRedefinition:
addNonNull= true;
builder.swapAnnotations();
break;
case IProblem.RequiredNonNullButProvidedNull:
case IProblem.RequiredNonNullButProvidedPotentialNull:
case IProblem.RequiredNonNullButProvidedUnknown:
case IProblem.RequiredNonNullButProvidedSpecdNullable:
if (isArgumentProblem) {
addNonNull= true;
builder.swapAnnotations();
}
break;
default:
// 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.
SignatureAnnotationRewriteOperation fix= builder.createAddAnnotationOperation(handledPositions, true/*thisUnitOnly*/, ChangeKind.LOCAL);
if (fix != null) {
if (addNonNull) {
fix.fRemoveIfNonNullByDefault= true;
fix.fNonNullByDefaultName= getNonNullByDefaultAnnotationName(compilationUnit.getJavaElement(), false);
}
result.add(fix);
}
}
}
private static void createRemoveRedundantNullAnnotationsOperations(CompilationUnit compilationUnit, IProblemLocation[] locations, List<CompilationUnitRewriteOperation> result) {
for (int i= 0; i < locations.length; i++) {
IProblemLocation problem= locations[i];
if (problem == null)
continue; // problem was filtered out by createCleanUp()
int problemId= problem.getProblemId();
if (problemId == IProblem.RedundantNullAnnotation || problemId == IProblem.RedundantNullDefaultAnnotationPackage || problemId == IProblem.RedundantNullDefaultAnnotationType
|| problemId == IProblem.RedundantNullDefaultAnnotationMethod || problemId == IProblem.RedundantNullDefaultAnnotationLocal
|| problemId == IProblem.RedundantNullDefaultAnnotationField) {
RemoveRedundantAnnotationRewriteOperation operation= new RemoveRedundantAnnotationRewriteOperation(compilationUnit, problem);
result.add(operation);
}
}
}
// private static boolean isMissingNullAnnotationProblem(int id) {
// return id == IProblem.RequiredNonNullButProvidedNull || id == IProblem.RequiredNonNullButProvidedPotentialNull || id == IProblem.IllegalReturnNullityRedefinition
// || mayIndicateParameterNullcheck(id);
// }
//
// private static boolean mayIndicateParameterNullcheck(int problemId) {
// return problemId == IProblem.NonNullLocalVariableComparisonYieldsFalse || problemId == IProblem.RedundantNullCheckOnNonNullLocalVariable;
// }
/**
* Tells whether an explicit null annotation exists on the given compilation unit.
*
* @param compilationUnit the compilation unit
* @param offset the offset
* @return <code>true</code> if the compilation unit has an explicit null annotation
*/
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) {
return getAnnotationName(javaElement, makeSimple, JavaCore.COMPILER_NULLABLE_ANNOTATION_NAME);
}
public static String getNonNullAnnotationName(IJavaElement javaElement, boolean makeSimple) {
return getAnnotationName(javaElement, makeSimple, JavaCore.COMPILER_NONNULL_ANNOTATION_NAME);
}
public static String getNonNullByDefaultAnnotationName(IJavaElement javaElement, boolean makeSimple) {
return getAnnotationName(javaElement, makeSimple, JavaCore.COMPILER_NONNULL_BY_DEFAULT_ANNOTATION_NAME);
}
private static String getAnnotationName(IJavaElement javaElement, boolean makeSimple, String annotation) {
String qualifiedName= javaElement.getJavaProject().getOption(annotation, true);
int lastDot;
if (makeSimple && qualifiedName != null && (lastDot= qualifiedName.lastIndexOf('.')) != -1)
return qualifiedName.substring(lastDot + 1);
return qualifiedName;
}
}