blob: 10eef0a2f8abae8545797104192260db994e402a [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2017, 2018 Till Brychcy 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:
* Till Brychcy - initial API and implementation
* Red Hat Inc. - refactored to jdt.core.manipulation
*******************************************************************************/
package org.eclipse.jdt.internal.corext.codemanipulation;
import java.lang.annotation.ElementType;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
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.IAnnotationBinding;
import org.eclipse.jdt.core.dom.IExtendedModifier;
import org.eclipse.jdt.core.dom.IMemberValuePairBinding;
import org.eclipse.jdt.core.dom.IModuleBinding;
import org.eclipse.jdt.core.dom.IPackageBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.PackageDeclaration;
import org.eclipse.jdt.core.dom.SingleVariableDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationExpression;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite.TypeLocation;
/* @NonNullByDefault */
public class RedundantNullnessTypeAnnotationsFilter {
private static final IAnnotationBinding[] NO_ANNOTATIONS= new IAnnotationBinding[0];
/**
* Locations where both {@code @NonNull} and {@code @Nullable} should always be filtered:
* <ul>
* <li>for local variables, nullity is inferred by flow analysis.
* <li>casts with null annotation would always be unchecked.
* <li>for exceptions, and {@code new} expressions non-null is implied.
* <li>in other mentioned locations null annotations are not supported for other reasons.
* </ul>
*/
private static final EnumSet<TypeLocation> NEVER_NULLNESS_LOCATIONS= EnumSet.of(
TypeLocation.LOCAL_VARIABLE, TypeLocation.CAST,
TypeLocation.EXCEPTION, TypeLocation.NEW,
TypeLocation.INSTANCEOF, TypeLocation.RECEIVER);
public static /* @Nullable */ RedundantNullnessTypeAnnotationsFilter createIfConfigured(/* @Nullable */ ASTNode node) {
if (node == null) {
return null;
}
final ASTNode root= node.getRoot();
if (root instanceof CompilationUnit) {
CompilationUnit compilationUnit= (CompilationUnit) root;
IJavaElement javaElement= compilationUnit.getJavaElement();
IJavaProject javaProject= javaElement == null ? null : javaElement.getJavaProject();
if (javaProject != null) {
if (JavaCore.ENABLED.equals(javaProject.getOption(JavaCore.COMPILER_ANNOTATION_NULL_ANALYSIS, true))) {
String nonNullAnnotationName= javaProject.getOption(JavaCore.COMPILER_NONNULL_ANNOTATION_NAME, true);
String nullableAnnotationName= javaProject.getOption(JavaCore.COMPILER_NULLABLE_ANNOTATION_NAME, true);
Set<String> nonNullByDefaultNames= determineNonNullByDefaultNames(javaProject);
if (nonNullAnnotationName == null || nullableAnnotationName == null || nonNullByDefaultNames == null) {
return null;
}
EnumSet<TypeLocation> nonNullByDefaultLocations= determineNonNullByDefaultLocations(node, nonNullByDefaultNames);
return new RedundantNullnessTypeAnnotationsFilter(nonNullAnnotationName, nullableAnnotationName, nonNullByDefaultLocations);
}
}
}
return null;
}
public static /* Nullable */ Set<String> determineNonNullByDefaultNames(IJavaProject javaProject) {
String nonNullByDefaultName= javaProject.getOption(JavaCore.COMPILER_NONNULL_BY_DEFAULT_ANNOTATION_NAME, true);
if (nonNullByDefaultName == null) {
return null;
}
LinkedHashSet<String> nonNullByDefaultNames= new LinkedHashSet<>();
nonNullByDefaultNames.add(nonNullByDefaultName);
String secondaryNNBDNames= javaProject.getOption(JavaCore.COMPILER_NONNULL_BY_DEFAULT_ANNOTATION_SECONDARY_NAMES, true);
if (secondaryNNBDNames != null) {
for (String string : secondaryNNBDNames.split(",")) { //$NON-NLS-1$
string= string.trim();
if (string.length() > 0) {
nonNullByDefaultNames.add(string);
}
}
}
return nonNullByDefaultNames;
}
public static EnumSet<TypeLocation> determineNonNullByDefaultLocations(ASTNode astNode, Set<String> nonNullByDefaultNames) {
// look for first @NonNullByDefault
while (astNode != null) {
List<IAnnotationBinding> annots= getNNBDAnnotations(astNode, nonNullByDefaultNames);
if (annots != null) {
return determineNNBDValue(annots);
}
astNode= astNode.getParent();
}
return EnumSet.noneOf(TypeLocation.class);
}
private static EnumSet<TypeLocation> determineNNBDValue(List<IAnnotationBinding> annots) {
EnumSet<TypeLocation> result= EnumSet.noneOf(TypeLocation.class);
for (IAnnotationBinding annot : annots) {
IMemberValuePairBinding[] pairs= annot.getAllMemberValuePairs();
if (pairs.length == 0) {
ITypeBinding annotationType= annot.getAnnotationType();
boolean foundTypeQualifierDefault= false;
if (annotationType != null) {
IAnnotationBinding[] annotationAnnotations= annotationType.getAnnotations();
for (IAnnotationBinding nestedBinding : annotationAnnotations) {
ITypeBinding nestedAnnotationType= nestedBinding.getAnnotationType();
if (nestedAnnotationType != null) {
if (nestedAnnotationType.getName().equals("TypeQualifierDefault")) { //$NON-NLS-1$
foundTypeQualifierDefault= true;
for (final IMemberValuePairBinding pair : nestedBinding.getAllMemberValuePairs()) {
if (pair.getKey() == null || pair.getKey().equals("value")) { //$NON-NLS-1$
Object value= pair.getValue();
if (value instanceof Object[]) {
Object[] values= (Object[]) value;
for (Object elem : values) {
addElementTypesAsTypeLocationValues(result, elem);
}
} else {
addElementTypesAsTypeLocationValues(result, value);
}
}
}
break;
}
}
}
}
if (!foundTypeQualifierDefault) {
// unconfigurable NonNullByDefault annotation
addAsTypeLocationValues(result, Boolean.TRUE);
}
} else {
for (final IMemberValuePairBinding pair : pairs) {
if (pair.getKey() == null || pair.getKey().equals("value")) { //$NON-NLS-1$
Object value= pair.getValue();
if (value instanceof Object[]) {
Object[] values= (Object[]) value;
for (Object elem : values) {
addAsTypeLocationValues(result, elem);
}
} else {
addAsTypeLocationValues(result, value);
}
}
}
}
}
return result;
}
private static void addAsTypeLocationValues(EnumSet<TypeLocation> result, Object value) {
if (value instanceof IVariableBinding) {
String name= ((IVariableBinding) value).getName();
try {
result.add(TypeLocation.valueOf(name));
} catch (IllegalArgumentException e) {
// ignore
}
} else if (value instanceof Boolean) {
if (((Boolean) value).booleanValue()) {
result.add(TypeLocation.RETURN_TYPE);
result.add(TypeLocation.PARAMETER);
result.add(TypeLocation.FIELD);
}
}
}
private static void addElementTypesAsTypeLocationValues(EnumSet<TypeLocation> result, Object value) {
if (value instanceof IVariableBinding) {
String name= ((IVariableBinding) value).getName();
if (ElementType.PARAMETER.name().equals(name)) {
result.add(TypeLocation.PARAMETER);
} else if (ElementType.METHOD.name().equals(name)) {
result.add(TypeLocation.RETURN_TYPE);
} else if (ElementType.FIELD.name().equals(name)) {
result.add(TypeLocation.FIELD);
}
}
}
// based on org.eclipse.jdt.apt.core.internal.declaration.ASTBasedDeclarationImpl.getAnnotationInstancesFromAST()
private static /* @Nullable */ List<IAnnotationBinding> getNNBDAnnotations(ASTNode astNode, Set<String> nonNullByDefaultNames) {
List<IExtendedModifier> extendsMods= null;
ArrayList<IAnnotationBinding> list= null;
switch (astNode.getNodeType()) {
case ASTNode.COMPILATION_UNIT: {
// special case: when reaching the root of the ast, check the package and module annotations.
PackageDeclaration packageDeclaration= ((CompilationUnit) astNode).getPackage();
if (packageDeclaration != null) {
IPackageBinding packageBinding= packageDeclaration.resolveBinding();
if (packageBinding != null) {
for (IAnnotationBinding annotationBinding : packageBinding.getAnnotations()) {
ITypeBinding annotationType= annotationBinding.getAnnotationType();
if (annotationType != null) {
if (nonNullByDefaultNames.contains(annotationType.getQualifiedName())) {
if (list == null)
list= new ArrayList<>();
list.add(annotationBinding);
}
}
}
if (list != null)
return list;
IModuleBinding module= packageBinding.getModule();
if (module != null) {
for (IAnnotationBinding annotationBinding : module.getAnnotations()) {
ITypeBinding annotationType= annotationBinding.getAnnotationType();
if (annotationType != null && nonNullByDefaultNames.contains(annotationType.getQualifiedName())) {
if (list == null)
list= new ArrayList<>();
list.add(annotationBinding);
}
}
if (list != null)
return list;
}
}
}
return null;
}
case ASTNode.TYPE_DECLARATION:
case ASTNode.ANNOTATION_TYPE_DECLARATION:
case ASTNode.ENUM_DECLARATION:
case ASTNode.ANNOTATION_TYPE_MEMBER_DECLARATION:
case ASTNode.METHOD_DECLARATION:
case ASTNode.FIELD_DECLARATION:
case ASTNode.ENUM_CONSTANT_DECLARATION:
extendsMods= ((BodyDeclaration) astNode).modifiers();
break;
case ASTNode.VARIABLE_DECLARATION_STATEMENT:
extendsMods= ((VariableDeclarationStatement) astNode).modifiers();
break;
case ASTNode.VARIABLE_DECLARATION_EXPRESSION:
extendsMods= ((VariableDeclarationExpression) astNode).modifiers();
break;
case ASTNode.SINGLE_VARIABLE_DECLARATION:
extendsMods= ((SingleVariableDeclaration) astNode).modifiers();
break;
case ASTNode.VARIABLE_DECLARATION_FRAGMENT:
final ASTNode parent= ((VariableDeclarationFragment) astNode).getParent();
if (parent instanceof BodyDeclaration)
extendsMods= ((BodyDeclaration) parent).modifiers();
break;
default:
return null;
}
if (extendsMods != null) {
for (IExtendedModifier extMod : extendsMods) {
if (extMod.isAnnotation()) {
Annotation annotation= (Annotation) extMod;
IAnnotationBinding annotationBinding= annotation.resolveAnnotationBinding();
if (annotationBinding != null) {
ITypeBinding annotationType= annotationBinding.getAnnotationType();
if (annotationType != null && nonNullByDefaultNames.contains(annotationType.getQualifiedName())) {
if (list == null)
list= new ArrayList<>();
list.add(annotationBinding);
}
}
}
}
if (list != null)
return list;
}
return null;
}
private final String fNonNullAnnotationName;
private final String fNullableAnnotationName;
private final EnumSet<TypeLocation> fNonNullByDefaultLocations;
public RedundantNullnessTypeAnnotationsFilter(String nonNullAnnotationName, String nullableAnnotationName, EnumSet<TypeLocation> nonNullByDefaultLocations) {
fNonNullAnnotationName= nonNullAnnotationName;
fNullableAnnotationName= nullableAnnotationName;
fNonNullByDefaultLocations= nonNullByDefaultLocations;
}
public IAnnotationBinding[] removeUnwantedTypeAnnotations(IAnnotationBinding[] annotations, TypeLocation location, ITypeBinding type) {
if (location == TypeLocation.OTHER)
return NO_ANNOTATIONS;
if (type.isTypeVariable() || type.isWildcardType()) {
return annotations;
}
boolean excludeAllNullAnnotations= NEVER_NULLNESS_LOCATIONS.contains(location);
if (excludeAllNullAnnotations || fNonNullByDefaultLocations.contains(location)) {
ArrayList<IAnnotationBinding> list= new ArrayList<>(annotations.length);
for (IAnnotationBinding annotation : annotations) {
ITypeBinding annotationType= annotation.getAnnotationType();
if (annotationType != null) {
if (annotationType.getQualifiedName().equals(fNonNullAnnotationName)) {
// ignore @NonNull
} else if (excludeAllNullAnnotations && annotationType.getQualifiedName().equals(fNullableAnnotationName)) {
// also ignore @Nullable
} else {
list.add(annotation);
}
} else {
list.add(annotation);
}
}
return list.size() == annotations.length ? annotations : list.toArray(new IAnnotationBinding[list.size()]);
} else {
return annotations;
}
}
}