/*
* 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.apache.commons.lang.StringUtils;
import org.sonar.check.Rule;
import org.sonar.check.RuleProperty;
import org.sonar.java.matcher.MethodMatcher;
import org.sonar.java.model.ExpressionUtils;
import org.sonar.java.se.CheckerContext;
import org.sonar.java.se.ExplodedGraph;
import org.sonar.java.se.ProgramState;
import org.sonar.java.se.SymbolicValueFactory;
import org.sonar.java.se.constraint.ConstraintManager;
import org.sonar.java.se.constraint.ObjectConstraint;
import org.sonar.java.se.symbolicvalues.SymbolicValue;
import org.sonar.plugins.java.api.JavaFileScannerContext;
import org.sonar.plugins.java.api.semantic.Symbol;
import org.sonar.plugins.java.api.semantic.Type;
import org.sonar.plugins.java.api.tree.Arguments;
import org.sonar.plugins.java.api.tree.AssignmentExpressionTree;
import org.sonar.plugins.java.api.tree.ExpressionTree;
import org.sonar.plugins.java.api.tree.IdentifierTree;
import org.sonar.plugins.java.api.tree.MemberSelectExpressionTree;
import org.sonar.plugins.java.api.tree.MethodInvocationTree;
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.TryStatementTree;
import org.sonar.plugins.java.api.tree.VariableTree;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Rule(key = "S2095")
public class UnclosedResourcesCheck extends SECheck {
private enum Status implements ObjectConstraint.Status {
OPENED, CLOSED
}
@RuleProperty(
key = "excludedResourceTypes",
description = "Comma separated list of the excluded resource types, using fully qualified names (example: \"org.apache.hadoop.fs.FileSystem\")",
defaultValue = "")
public String excludedTypes = "";
private final List<String> excludedTypesList = new ArrayList<>();
private static final String JAVA_IO_AUTO_CLOSEABLE = "java.lang.AutoCloseable";
private static final String JAVA_IO_CLOSEABLE = "java.io.Closeable";
private static final String JAVA_SQL_STATEMENT = "java.sql.Statement";
private static final String[] JDBC_RESOURCE_CREATIONS = {"java.sql.Connection", JAVA_SQL_STATEMENT};
private static final String STREAM_TOP_HIERARCHY = "java.util.stream.BaseStream";
private static final String[] IGNORED_CLOSEABLE_SUBTYPES = {
"java.io.ByteArrayOutputStream",
"java.io.ByteArrayInputStream",
"java.io.CharArrayReader",
"java.io.CharArrayWriter",
"java.io.StringReader",
"java.io.StringWriter",
"com.sun.org.apache.xml.internal.security.utils.UnsyncByteArrayOutputStream",
"org.springframework.context.ConfigurableApplicationContext"
};
private static final MethodMatcher[] CLOSEABLE_EXCEPTIONS = new MethodMatcher[] {
MethodMatcher.create().typeDefinition("java.nio.file.FileSystems").name("getDefault").withoutParameter()
};
@Override
public ProgramState checkPreStatement(CheckerContext context, Tree syntaxNode) {
final PreStatementVisitor visitor = new PreStatementVisitor(context);
syntaxNode.accept(visitor);
return visitor.programState;
}
@Override
public ProgramState checkPostStatement(CheckerContext context, Tree syntaxNode) {
final PostStatementVisitor visitor = new PostStatementVisitor(context);
syntaxNode.accept(visitor);
return visitor.programState;
}
@Override
public void checkEndOfExecutionPath(CheckerContext context, ConstraintManager constraintManager) {
ExplodedGraph.Node node = context.getNode();
context.getState().getValuesWithConstraints(Status.OPENED)
.forEach((sv, c) -> processUnclosedSymbolicValue(node, sv));
}
private void processUnclosedSymbolicValue(ExplodedGraph.Node node, SymbolicValue sv) {
FlowComputation.flow(node, sv, ObjectConstraint.statusPredicate(Status.OPENED)).stream()
.filter(location -> location.syntaxNode.is(Tree.Kind.NEW_CLASS, Tree.Kind.METHOD_INVOCATION))
.forEach(this::reportIssue);
}
private void reportIssue(JavaFileScannerContext.Location location) {
String message = "Close this \"" + name(location.syntaxNode) + "\".";
String flowMessage = name(location.syntaxNode) + " is never closed";
Set<List<JavaFileScannerContext.Location>> flows = FlowComputation.singleton(flowMessage, location.syntaxNode);
reportIssue(location.syntaxNode, message, flows);
}
private static String name(Tree tree) {
if (tree.is(Tree.Kind.NEW_CLASS)) {
return ((NewClassTree) tree).symbolType().name();
}
return ((MethodInvocationTree) tree).symbolType().name();
}
private boolean needsClosing(Type type) {
if (type.isSubtypeOf(STREAM_TOP_HIERARCHY)) {
return false;
}
for (String ignoredTypes : IGNORED_CLOSEABLE_SUBTYPES) {
if (type.isSubtypeOf(ignoredTypes)) {
return false;
}
}
for (String excludedType : loadExcludedTypesList()) {
if (type.is(excludedType)) {
return false;
}
}
return isCloseable(type);
}
private List<String> loadExcludedTypesList() {
if ( excludedTypesList.isEmpty() && !StringUtils.isBlank(excludedTypes)) {
for (String excludedType : excludedTypes.split(",")) {
excludedTypesList.add(excludedType.trim());
}
}
return excludedTypesList;
}
private static boolean isCloseable(Type type) {
return type.isSubtypeOf(JAVA_IO_AUTO_CLOSEABLE) || type.isSubtypeOf(JAVA_IO_CLOSEABLE);
}
private boolean isOpeningResource(NewClassTree syntaxNode) {
if (isWithinTryHeader(syntaxNode)) {
return false;
}
return needsClosing(syntaxNode.symbolType());
}
private static boolean isWithinTryHeader(Tree syntaxNode) {
final Tree parent = syntaxNode.parent();
if (parent.is(Tree.Kind.VARIABLE)) {
return isTryStatementResource((VariableTree) parent);
}
return false;
}
private static boolean isTryStatementResource(VariableTree variable) {
final TryStatementTree tryStatement = getEnclosingTryStatement(variable);
return tryStatement != null && tryStatement.resources().contains(variable);
}
private static TryStatementTree getEnclosingTryStatement(Tree syntaxNode) {
Tree parent = syntaxNode.parent();
while (parent != null) {
if (parent.is(Tree.Kind.TRY_STATEMENT)) {
return (TryStatementTree) parent;
}
parent = parent.parent();
}
return null;
}
private static class ResourceWrapperSymbolicValue extends SymbolicValue {
private final SymbolicValue dependent;
ResourceWrapperSymbolicValue(int id, SymbolicValue dependent) {
super(id);
this.dependent = dependent;
}
@Override
public SymbolicValue wrappedValue() {
return dependent.wrappedValue();
}
}
private static class WrappedValueFactory implements SymbolicValueFactory {
private final SymbolicValue value;
WrappedValueFactory(SymbolicValue value) {
this.value = value;
}
@Override
public SymbolicValue createSymbolicValue(int counter) {
return new ResourceWrapperSymbolicValue(counter, value);
}
}
private class PreStatementVisitor extends CheckerTreeNodeVisitor {
// closing methods
private static final String CLOSE = "close";
private static final String GET_MORE_RESULTS = "getMoreResults";
// opening resources method
private static final String GET_RESULT_SET = "getResultSet";
private final ConstraintManager constraintManager;
PreStatementVisitor(CheckerContext context) {
super(context.getState());
constraintManager = context.getConstraintManager();
}
@Override
public void visitNewClass(NewClassTree syntaxNode) {
List<SymbolicValue> arguments = new ArrayList<>(programState.peekValues(syntaxNode.arguments().size()));
Collections.reverse(arguments);
if (isOpeningResource(syntaxNode)) {
Iterator<SymbolicValue> iterator = arguments.iterator();
for (ExpressionTree argument : syntaxNode.arguments()) {
if (!iterator.hasNext()) {
throw new IllegalStateException("Mismatch between declared constructor arguments and argument values!");
}
final Type type = argument.symbolType();
final SymbolicValue value = iterator.next();
if (isCloseable(type)) {
constraintManager.setValueFactory(new WrappedValueFactory(value));
break;
}
}
} else {
closeArguments(syntaxNode.arguments());
}
}
@Override
public void visitReturnStatement(ReturnStatementTree syntaxNode) {
SymbolicValue currentVal = programState.peekValue();
if (currentVal != null) {
final ExpressionTree expression = syntaxNode.expression();
if (expression != null) {
if (expression.is(Tree.Kind.IDENTIFIER)) {
final IdentifierTree identifier = (IdentifierTree) expression;
currentVal = programState.getValue(identifier.symbol());
} else {
currentVal = programState.peekValue();
}
closeResource(currentVal);
}
}
}
@Override
public void visitAssignmentExpression(AssignmentExpressionTree syntaxNode) {
final ExpressionTree variable = syntaxNode.variable();
if (isNonLocalStorage(variable)) {
SymbolicValue value;
if (ExpressionUtils.isSimpleAssignment(syntaxNode)) {
value = programState.peekValue();
} else {
value = programState.peekValues(2).get(0);
}
closeResource(value);
}
}
private boolean isNonLocalStorage(ExpressionTree variable) {
if (variable.is(Tree.Kind.IDENTIFIER)) {
Symbol owner = ((IdentifierTree) variable).symbol().owner();
return !owner.isMethodSymbol();
}
return true;
}
@Override
public void visitMethodInvocation(MethodInvocationTree syntaxNode) {
Symbol symbol = syntaxNode.symbol();
if (symbol.isMethodSymbol() && syntaxNode.methodSelect().is(Tree.Kind.MEMBER_SELECT)) {
String methodName = symbol.name();
SymbolicValue value = getTargetValue(syntaxNode);
if (CLOSE.equals(methodName)) {
closeResource(value);
} else if (GET_MORE_RESULTS.equals(methodName)) {
closeResultSetsRelatedTo(value);
} else if (GET_RESULT_SET.equals(methodName)) {
constraintManager.setValueFactory(new WrappedValueFactory(value));
}
}
// close any resource used as argument, even for unknown methods
closeArguments(syntaxNode.arguments());
}
private SymbolicValue getTargetValue(MethodInvocationTree syntaxNode) {
ExpressionTree targetExpression = ((MemberSelectExpressionTree) syntaxNode.methodSelect()).expression();
SymbolicValue value;
if (targetExpression.is(Tree.Kind.IDENTIFIER)) {
IdentifierTree identifier = (IdentifierTree) targetExpression;
value = programState.getValue(identifier.symbol());
} else {
value = programState.peekValue();
}
return value;
}
private void closeResultSetsRelatedTo(SymbolicValue value) {
for (Map.Entry<SymbolicValue, ObjectConstraint> constrainedValue : programState.getValuesWithConstraints(Status.OPENED).entrySet()) {
if (constrainedValue.getKey() instanceof ResourceWrapperSymbolicValue) {
ResourceWrapperSymbolicValue rValue = (ResourceWrapperSymbolicValue) constrainedValue.getKey();
if (value.equals(rValue.dependent)) {
programState = programState.addConstraint(rValue, constrainedValue.getValue().withStatus(Status.CLOSED));
}
}
}
}
private void closeArguments(final Arguments arguments) {
programState.peekValues(arguments.size()).forEach(this::closeResource);
}
private void closeResource(@Nullable final SymbolicValue target) {
if (target != null) {
ObjectConstraint oConstraint = programState.getConstraintWithStatus(target, Status.OPENED);
if (oConstraint != null) {
programState = programState.addConstraint(target.wrappedValue(), oConstraint.withStatus(Status.CLOSED));
}
}
}
}
private class PostStatementVisitor extends CheckerTreeNodeVisitor {
PostStatementVisitor(CheckerContext context) {
super(context.getState());
}
@Override
public void visitNewClass(NewClassTree syntaxNode) {
if (isOpeningResource(syntaxNode)) {
final SymbolicValue instanceValue = programState.peekValue();
if (!(instanceValue instanceof ResourceWrapperSymbolicValue)) {
programState = programState.addConstraint(instanceValue, new ObjectConstraint(false, false, Status.OPENED));
}
}
}
@Override
public void visitMethodInvocation(MethodInvocationTree syntaxNode) {
for (MethodMatcher matcher : CLOSEABLE_EXCEPTIONS) {
if (matcher.matches(syntaxNode)) {
return;
}
}
if (syntaxNode.methodSelect().is(Tree.Kind.MEMBER_SELECT) && needsClosing(syntaxNode.symbolType())) {
final ExpressionTree targetExpression = ((MemberSelectExpressionTree) syntaxNode.methodSelect()).expression();
if (targetExpression.is(Tree.Kind.IDENTIFIER) && !isWithinTryHeader(syntaxNode)
&& (syntaxNode.symbol().isStatic() || isJdbcResourceCreation(targetExpression))) {
programState = programState.addConstraint(programState.peekValue(), new ObjectConstraint(false, false, Status.OPENED));
}
}
}
private boolean isJdbcResourceCreation(ExpressionTree targetExpression) {
for (String creator : JDBC_RESOURCE_CREATIONS) {
if (targetExpression.symbolType().is(creator)) {
return true;
}
}
return false;
}
}
}