/*
* SonarQube Java
* Copyright (C) 2012-2016 SonarSource SA
* mailto:contact AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.java.se.checks;
import org.sonar.check.Rule;
import org.sonar.java.cfg.CFG;
import org.sonar.java.model.ExpressionUtils;
import org.sonar.java.resolve.JavaSymbol;
import org.sonar.java.resolve.Scope;
import org.sonar.java.se.CheckerContext;
import org.sonar.java.se.ProgramState;
import org.sonar.java.se.constraint.Constraint;
import org.sonar.java.se.constraint.ConstraintManager;
import org.sonar.java.se.symbolicvalues.SymbolicValue;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.SymbolMetadata;
import org.sonar.plugins.java.api.tree.Arguments;
import org.sonar.plugins.java.api.tree.AssignmentExpressionTree;
import org.sonar.plugins.java.api.tree.ClassTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.IdentifierTree;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
import org.sonar.plugins.java.api.tree.MethodTree;
import org.sonar.plugins.java.api.tree.NewClassTree;
import org.sonar.plugins.java.api.tree.ReturnStatementTree;
import org.sonar.plugins.java.api.tree.Tree;
import org.sonar.plugins.java.api.tree.VariableTree;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
@Rule(key = "S2637")
public class NonNullSetToNullCheck extends SECheck {
private static final String[] ANNOTATIONS = {"javax.annotation.Nonnull", "javax.validation.constraints.NotNull",
"edu.umd.cs.findbugs.annotations.NonNull", "org.jetbrains.annotations.NotNull", "lombok.NonNull",
"android.support.annotation.NonNull"};
private static final String[] JPA_ANNOTATIONS = {
"javax.persistence.Entity",
"javax.persistence.Embeddable"
};
private MethodTree methodTree;
@Override
public void init(MethodTree tree, CFG cfg) {
methodTree = tree;
}
@Override
public ProgramState checkPreStatement(CheckerContext context, Tree syntaxNode) {
AbstractStatementVisitor visitor = new PreStatementVisitor(context);
syntaxNode.accept(visitor);
return visitor.programState;
}
@Override
public ProgramState checkPostStatement(CheckerContext context, Tree syntaxNode) {
AbstractStatementVisitor visitor = new PostStatementVisitor(context);
syntaxNode.accept(visitor);
return visitor.programState;
}
@Override
public void checkEndOfExecutionPath(CheckerContext context, ConstraintManager constraintManager) {
if (methodTree.is(Tree.Kind.CONSTRUCTOR) && !isDefaultConstructorForJpa(methodTree)) {
ClassTree classTree = (ClassTree) methodTree.parent();
for (Tree member : classTree.members()) {
if (member.is(Tree.Kind.VARIABLE)) {
checkVariable(context, methodTree, ((VariableTree) member).symbol());
}
}
}
}
private static boolean isDefaultConstructorForJpa(MethodTree methodTree) {
if (!methodTree.block().body().isEmpty()) {
// Constructor does something.
return false;
}
SymbolMetadata symbolMetadata = ((ClassTree) methodTree.parent()).symbol().metadata();
return Stream.of(JPA_ANNOTATIONS).anyMatch(symbolMetadata::isAnnotatedWith);
}
private void checkVariable(CheckerContext context, MethodTree tree, final Symbol symbol) {
String nonNullAnnotation = nonNullAnnotation(symbol);
if (nonNullAnnotation != null && isUndefinedOrNull(context, symbol)) {
context.reportIssue(tree, this,
MessageFormat.format("\"{0}\" is marked \"{1}\" but is not initialized in this constructor.", symbol.name(), nonNullAnnotation));
}
}
private static boolean isUndefinedOrNull(CheckerContext context, Symbol symbol) {
ProgramState programState = context.getState();
SymbolicValue value = programState.getValue(symbol);
return value == null;
}
@CheckForNull
private static String nonNullAnnotation(Symbol javaSymbol) {
for (String annotation : ANNOTATIONS) {
if (javaSymbol.metadata().isAnnotatedWith(annotation)) {
return annotation;
}
}
return null;
}
private abstract class AbstractStatementVisitor extends CheckerTreeNodeVisitor {
private final CheckerContext context;
protected AbstractStatementVisitor(CheckerContext context) {
super(context.getState());
this.context = context;
}
protected void reportIssue(Tree tree, String message, Object... parameters) {
context.reportIssue(tree, NonNullSetToNullCheck.this, MessageFormat.format(message, parameters));
}
}
private class PreStatementVisitor extends AbstractStatementVisitor {
protected PreStatementVisitor(CheckerContext context) {
super(context);
}
@Override
public void visitAssignmentExpression(AssignmentExpressionTree tree) {
if (ExpressionUtils.isSimpleAssignment(tree)) {
IdentifierTree variable = (IdentifierTree) ExpressionUtils.skipParentheses(tree.variable());
Symbol symbol = variable.symbol();
String nonNullAnnotation = nonNullAnnotation(symbol);
if (nonNullAnnotation != null) {
SymbolicValue assignedValue = programState.peekValue();
Constraint constraint = programState.getConstraint(assignedValue);
if (constraint != null && constraint.isNull()) {
reportIssue(tree, "\"{0}\" is marked \"{1}\" but is set to null.", symbol.name(), nonNullAnnotation);
}
}
}
}
@Override
public void visitNewClass(NewClassTree syntaxTree) {
Symbol symbol = syntaxTree.constructorSymbol();
if (symbol.isMethodSymbol()) {
int peekSize = syntaxTree.arguments().size();
List<SymbolicValue> argumentValues = new ArrayList<>(programState.peekValues(peekSize));
JavaSymbol.MethodJavaSymbol methodSymbol = (JavaSymbol.MethodJavaSymbol) symbol;
Collections.reverse(argumentValues);
checkNullArguments(syntaxTree, methodSymbol.getParameters(),
argumentValues, "Parameter {0} to this constructor is marked \"{1}\" but null is passed.");
}
}
@Override
public void visitMethodInvocation(MethodInvocationTree syntaxTree) {
Symbol symbol = syntaxTree.symbol();
if (symbol.isMethodSymbol()) {
Arguments arguments = syntaxTree.arguments();
int peekSize = arguments.size() + 1;
List<SymbolicValue> argumentValues = new ArrayList<>(programState.peekValues(peekSize));
argumentValues.remove(arguments.size());
Collections.reverse(argumentValues);
JavaSymbol.MethodJavaSymbol methodSymbol = (JavaSymbol.MethodJavaSymbol) symbol;
checkNullArguments(syntaxTree, methodSymbol.getParameters(),
argumentValues, "Parameter {0} to this call is marked \"{1}\" but null is passed.");
}
}
protected void checkNullArguments(Tree syntaxTree, @Nullable Scope parameters, List<SymbolicValue> argumentValues, String message) {
if (parameters != null) {
List<JavaSymbol> scopeSymbols = parameters.scopeSymbols();
int parametersToTest = argumentValues.size();
if (scopeSymbols.size() < parametersToTest) {
// The last parameter is a variable length argument: the non-null condition does not apply to its values
parametersToTest = scopeSymbols.size() - 1;
}
for (int i = 0; i < parametersToTest; i++) {
checkNullArgument(syntaxTree, message, scopeSymbols, argumentValues, i);
}
}
}
protected void checkNullArgument(Tree syntaxTree, String message, List<JavaSymbol> scopeSymbols, List<SymbolicValue> argumentValues, int i) {
String nonNullAnnotation = nonNullAnnotation(scopeSymbols.get(i));
if (nonNullAnnotation != null) {
Constraint constraint = programState.getConstraint(argumentValues.get(i));
if (constraint != null && constraint.isNull()) {
reportIssue(syntaxTree, message, Integer.valueOf(i + 1), nonNullAnnotation);
}
}
}
}
private class PostStatementVisitor extends AbstractStatementVisitor {
protected PostStatementVisitor(CheckerContext context) {
super(context);
}
@Override
public void visitReturnStatement(ReturnStatementTree tree) {
Tree parent = tree.parent();
while (!parent.is(Tree.Kind.METHOD)) {
parent = parent.parent();
if (parent == null) {
// This occurs when the return statement is within a constructor
return;
}
}
MethodTree mTree = (MethodTree) parent;
String nonNullAnnotation = nonNullAnnotation(mTree.symbol());
if (nonNullAnnotation != null && isLocalExpression(tree.expression())) {
checkReturnedValue(tree, nonNullAnnotation);
}
}
private boolean isLocalExpression(@Nullable ExpressionTree expression) {
if (expression == null) {
return false;
}
if (expression.is(Tree.Kind.IDENTIFIER)) {
final Symbol symbol = ((IdentifierTree) expression).symbol().owner();
return symbol.isMethodSymbol();
}
return true;
}
private void checkReturnedValue(ReturnStatementTree tree, String nonNullAnnotation) {
SymbolicValue returnedValue = programState.peekValue();
Constraint constraint = programState.getConstraint(returnedValue);
if (constraint != null && constraint.isNull()) {
reportIssue(tree, "This method''s return value is marked \"{0}\" but null is returned.", nonNullAnnotation);
}
}
}
}