/*
* FindBugs - Find bugs in Java programs
* Copyright (C) 2003,2004 University of Maryland
*
* This library 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 2.1 of the License, or (at your option) any later version.
*
* This library 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 library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package edu.umd.cs.findbugs.detect;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import org.apache.bcel.Constants;
import org.apache.bcel.classfile.Constant;
import org.apache.bcel.classfile.ConstantInterfaceMethodref;
import org.apache.bcel.classfile.ConstantMethodref;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.MethodGen;
import org.apache.bcel.generic.ObjectType;
import org.apache.bcel.generic.Type;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.ResourceCollection;
import edu.umd.cs.findbugs.ResourceTrackingDetector;
import edu.umd.cs.findbugs.SourceLineAnnotation;
import edu.umd.cs.findbugs.StatelessDetector;
import edu.umd.cs.findbugs.SystemProperties;
import edu.umd.cs.findbugs.TypeAnnotation;
import edu.umd.cs.findbugs.ba.CFG;
import edu.umd.cs.findbugs.ba.CFGBuilderException;
import edu.umd.cs.findbugs.ba.ClassContext;
import edu.umd.cs.findbugs.ba.Dataflow;
import edu.umd.cs.findbugs.ba.DataflowAnalysisException;
import edu.umd.cs.findbugs.ba.Hierarchy;
import edu.umd.cs.findbugs.ba.Location;
import edu.umd.cs.findbugs.ba.ObjectTypeFactory;
import edu.umd.cs.findbugs.ba.ResourceValueAnalysis;
//import edu.umd.cs.findbugs.ba.ResourceValueAnalysisTestDriver;
import edu.umd.cs.findbugs.ba.ResourceValueFrame;
/**
* A Detector to look for streams that are opened in a method,
* do not escape the method, and are not closed on all paths
* out of the method. Note that "stream" is a bit misleading,
* since we also use the detector to look for database resources
* that aren't closed.
*
* @author David Hovemeyer
*/
public final class FindOpenStream extends ResourceTrackingDetector<Stream, StreamResourceTracker> implements StatelessDetector {
static final boolean DEBUG = SystemProperties.getBoolean("fos.debug");
static final boolean IGNORE_WRAPPED_UNINTERESTING_STREAMS = !SystemProperties.getBoolean("fos.allowWUS");
/* ----------------------------------------------------------------------
* Tracked resource types
* ---------------------------------------------------------------------- */
/**
* List of base classes of tracked resources.
*/
static final ObjectType[] streamBaseList =
{ObjectTypeFactory.getInstance("java.io.InputStream"),
ObjectTypeFactory.getInstance("java.io.OutputStream"),
ObjectTypeFactory.getInstance("java.io.Reader"),
ObjectTypeFactory.getInstance("java.io.Writer"),
ObjectTypeFactory.getInstance("java.sql.Connection"),
ObjectTypeFactory.getInstance("java.sql.PreparedStatement"),
ObjectTypeFactory.getInstance("java.sql.Statement"),
ObjectTypeFactory.getInstance("java.sql.ResultSet")};
/**
* StreamFactory objects used to detect resources
* created within analyzed methods.
*/
static final StreamFactory[] streamFactoryList;
static {
ArrayList<StreamFactory> streamFactoryCollection = new ArrayList<StreamFactory>();
// Examine InputStreams, OutputStreams, Readers, and Writers,
// ignoring byte array, object stream, char array, and String variants.
streamFactoryCollection.add(new IOStreamFactory("java.io.InputStream",
new String[]{"java.io.ByteArrayInputStream", "java.io.StringBufferInputStream", "java.io.PipedInputStream"
,"java.io.ObjectInputStream"
},
"OS_OPEN_STREAM"));
streamFactoryCollection.add(new IOStreamFactory("java.io.OutputStream",
new String[]{"java.io.ByteArrayOutputStream", "java.io.PipedOutputStream"
, "java.io.ObjectOutputStream"
},
"OS_OPEN_STREAM"));
streamFactoryCollection.add(new IOStreamFactory("java.io.Reader",
new String[]{"java.io.StringReader", "java.io.CharArrayReader", "java.io.PipedReader"},
"OS_OPEN_STREAM"));
streamFactoryCollection.add(new IOStreamFactory("java.io.Writer",
new String[]{"java.io.StringWriter", "java.io.CharArrayWriter", "java.io.PipedWriter"},
"OS_OPEN_STREAM"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.lang.Class",
"getResourceAsStream", "(Ljava/lang/String;)Ljava/io/InputStream;",
"OS_OPEN_STREAM"));
// Ignore socket input and output streams
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.net.Socket",
"getInputStream", "()Ljava/io/InputStream;"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.net.Socket",
"getOutputStream", "()Ljava/io/OutputStream;"));
// Ignore System.{in,out,err}
streamFactoryCollection.add(new StaticFieldLoadStreamFactory("java.io.InputStream",
"java.lang.System", "in", "Ljava/io/InputStream;"));
streamFactoryCollection.add(new StaticFieldLoadStreamFactory("java.io.OutputStream",
"java.lang.System", "out", "Ljava/io/PrintStream;"));
streamFactoryCollection.add(new StaticFieldLoadStreamFactory("java.io.OutputStream",
"java.lang.System", "err", "Ljava/io/PrintStream;"));
// Ignore input streams loaded from instance fields
streamFactoryCollection.add(new InstanceFieldLoadStreamFactory("java.io.InputStream"));
streamFactoryCollection.add(new InstanceFieldLoadStreamFactory("java.io.Reader"));
// Ignore output streams loaded from instance fields.
// FIXME: what we really should do here is ignore the stream
// loaded from the field, but report any streams that wrap
// it. This is an important and useful distinction that the
// detector currently doesn't handle. Should be fairly
// easy to add.
streamFactoryCollection.add(new InstanceFieldLoadStreamFactory("java.io.OutputStream"));
streamFactoryCollection.add(new InstanceFieldLoadStreamFactory("java.io.Writer"));
// JDBC objects
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.Connection",
"prepareStatement", "(Ljava/lang/String;)Ljava/sql/PreparedStatement;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.Connection",
"prepareStatement", "(Ljava/lang/String;I)Ljava/sql/PreparedStatement;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.Connection",
"prepareStatement", "(Ljava/lang/String;[I)Ljava/sql/PreparedStatement;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.Connection",
"prepareStatement", "(Ljava/lang/String;II)Ljava/sql/PreparedStatement;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.Connection",
"prepareStatement", "(Ljava/lang/String;III)Ljava/sql/PreparedStatement;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.Connection",
"prepareStatement", "(Ljava/lang/String;[Ljava/lang/String;)Ljava/sql/PreparedStatement;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.Connection",
"prepareCall", "(Ljava/lang/String;)Ljava/sql/CallableStatement;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.Connection",
"prepareCall", "(Ljava/lang/String;II)Ljava/sql/CallableStatement;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.Connection",
"prepareCall", "(Ljava/lang/String;III)Ljava/sql/CallableStatement;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.DriverManager",
"getConnection", "(Ljava/lang/String;)Ljava/sql/Connection;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.DriverManager",
"getConnection", "(Ljava/lang/String;Ljava/util/Properties;)Ljava/sql/Connection;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.DriverManager",
"getConnection",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/sql/Connection;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("javax.sql.DataSource",
"getConnection",
"()Ljava/sql/Connection;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("javax.sql.DataSource",
"getConnection",
"(Ljava/lang/String;Ljava/lang/String;)Ljava/sql/Connection;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.Connection",
"createStatement", "()Ljava/sql/Statement;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.Connection",
"createStatement", "(II)Ljava/sql/Statement;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.Connection",
"createStatement", "(III)Ljava/sql/Statement;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.Connection",
"createStatement", "(Ljava/lang/String;)Ljava/sql/PreparedStatement;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.Connection",
"createStatement", "(Ljava/lang/String;I)Ljava/sql/PreparedStatement;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.Connection",
"createStatement", "(Ljava/lang/String;II)Ljava/sql/PreparedStatement;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.Connection",
"createStatement", "(Ljava/lang/String;III)Ljava/sql/PreparedStatement;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.Connection",
"createStatement", "(Ljava/lang/String;[I)Ljava/sql/PreparedStatement;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryCollection.add(new MethodReturnValueStreamFactory("java.sql.Connection",
"createStatement", "(Ljava/lang/String;[Ljava/lang/String;)Ljava/sql/PreparedStatement;",
"ODR_OPEN_DATABASE_RESOURCE"));
streamFactoryList = streamFactoryCollection.toArray(new StreamFactory[streamFactoryCollection.size()]);
}
/* ----------------------------------------------------------------------
* Helper classes
* ---------------------------------------------------------------------- */
private static class PotentialOpenStream {
public final String bugType;
public final int priority;
public final Stream stream;
public PotentialOpenStream(String bugType, int priority, Stream stream) {
this.bugType = bugType;
this.priority = priority;
this.stream = stream;
}
}
/* ----------------------------------------------------------------------
* Fields
* ---------------------------------------------------------------------- */
private List<PotentialOpenStream> potentialOpenStreamList;
/* ----------------------------------------------------------------------
* Implementation
* ---------------------------------------------------------------------- */
public FindOpenStream(BugReporter bugReporter) {
super(bugReporter);
this.potentialOpenStreamList = new LinkedList<PotentialOpenStream>();
}
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
// List of words that must appear in names of classes which
// create possible resources to be tracked. If we don't see a
// class containing one of these words, then we don't run the
// detector on the class.
private static final String[] PRESCREEN_CLASS_LIST =
{ "Stream", "Reader", "Writer", "DriverManager", "Connection", "Statement" };
/* (non-Javadoc)
* @see edu.umd.cs.findbugs.Detector#visitClassContext(edu.umd.cs.findbugs.ba.ClassContext)
*/
@Override
public void visitClassContext(ClassContext classContext) {
JavaClass jclass = classContext.getJavaClass();
// Check to see if the class references any other classes
// which could be resources we want to track.
// If we don't find any such classes, we skip analyzing
// the class. (Note: could do this by method.)
boolean sawResourceClass = false;
for (int i = 0; i < jclass.getConstantPool().getLength(); ++i) {
Constant constant = jclass.getConstantPool().getConstant(i);
String className = null;
if (constant instanceof ConstantMethodref) {
ConstantMethodref cmr = (ConstantMethodref) constant;
int classIndex = cmr.getClassIndex();
className = jclass.getConstantPool().getConstantString(
classIndex, Constants.CONSTANT_Class);
} else if (constant instanceof ConstantInterfaceMethodref) {
ConstantInterfaceMethodref cmr = (ConstantInterfaceMethodref) constant;
int classIndex = cmr.getClassIndex();
className = jclass.getConstantPool().getConstantString(
classIndex, Constants.CONSTANT_Class);
}
if (className != null) {
if (DEBUG) System.out.println("FindOpenStream: saw class " + className);
for (String aPRESCREEN_CLASS_LIST : PRESCREEN_CLASS_LIST) {
if (className.indexOf(aPRESCREEN_CLASS_LIST) >= 0) {
sawResourceClass = true;
break;
}
}
}
}
if (sawResourceClass) {
super.visitClassContext(classContext);
}
}
@Override
public boolean prescreen(ClassContext classContext, Method method, boolean mightClose) {
BitSet bytecodeSet = classContext.getBytecodeSet(method);
if (bytecodeSet == null) return false;
return bytecodeSet.get(Constants.NEW)
|| bytecodeSet.get(Constants.INVOKEINTERFACE)
|| bytecodeSet.get(Constants.INVOKESPECIAL)
|| bytecodeSet.get(Constants.INVOKESTATIC)
|| bytecodeSet.get(Constants.INVOKEVIRTUAL);
}
@Override
public StreamResourceTracker getResourceTracker(ClassContext classContext, Method method) {
return new StreamResourceTracker(streamFactoryList, bugReporter);
}
public static boolean isMainMethod(Method method) {
return method.isStatic()
&& method.getName().equals("main")
&& method.getSignature().equals("([Ljava/lang/String;)V");
}
@Override
public void analyzeMethod(ClassContext classContext, Method method,
StreamResourceTracker resourceTracker,
ResourceCollection<Stream> resourceCollection)
throws CFGBuilderException, DataflowAnalysisException {
potentialOpenStreamList.clear();
JavaClass javaClass = classContext.getJavaClass();
MethodGen methodGen = classContext.getMethodGen(method);
if (methodGen == null) return;
CFG cfg = classContext.getCFG(method);
// Add Streams passed into the method as parameters.
// These are uninteresting, and should poison
// any streams which wrap them.
try {
Type[] parameterTypeList = Type.getArgumentTypes(methodGen.getSignature());
Location firstLocation = new Location(cfg.getEntry().getFirstInstruction(), cfg.getEntry());
int local = methodGen.isStatic() ? 0 : 1;
for (Type type : parameterTypeList) {
if (type instanceof ObjectType) {
ObjectType objectType = (ObjectType) type;
for (ObjectType streamBase : streamBaseList) {
if (Hierarchy.isSubtype(objectType, streamBase)) {
// OK, found a parameter that is a resource.
// Create a Stream object to represent it.
// The Stream will be uninteresting, so it will
// inhibit reporting for any stream that wraps it.
Stream paramStream =
new Stream(firstLocation, objectType.getClassName(), streamBase.getClassName());
paramStream.setIsOpenOnCreation(true);
paramStream.setOpenLocation(firstLocation);
paramStream.setInstanceParam(local);
resourceCollection.addPreexistingResource(paramStream);
break;
}
}
}
switch (type.getType()) {
case Constants.T_LONG:
case Constants.T_DOUBLE:
local += 2;
break;
default:
local += 1;
break;
}
}
} catch (ClassNotFoundException e) {
bugReporter.reportMissingClass(e);
}
// Set precomputed map of Locations to Stream creation points.
// That way, the StreamResourceTracker won't have to
// repeatedly try to figure out where Streams are created.
resourceTracker.setResourceCollection(resourceCollection);
super.analyzeMethod(classContext, method, resourceTracker, resourceCollection);
// Compute streams that escape into other streams:
// this takes wrapper streams into account.
// This will also compute equivalence classes of streams,
// so that if one stream in a class is closed,
// they are all considered closed.
// (FIXME: this is too simplistic, especially if buffering
// is involved. Sometime we should really think harder
// about how this should work.)
resourceTracker.markTransitiveUninterestingStreamEscapes();
// For each stream closed on all paths, mark its equivalence
// class as being closed.
for (Iterator<Stream> i = resourceCollection.resourceIterator(); i.hasNext();) {
Stream stream = i.next();
StreamEquivalenceClass equivalenceClass = resourceTracker.getStreamEquivalenceClass(stream);
if (stream.isClosed())
equivalenceClass.setClosed();
}
// Iterate through potential open streams, reporting warnings
// for the "interesting" streams that haven't been closed
// (and aren't in an equivalence class with another stream
// that was closed).
for (PotentialOpenStream pos : potentialOpenStreamList) {
Stream stream = pos.stream;
if (stream.isClosed())
// Stream was in an equivalence class with another
// stream that was properly closed.
continue;
if (stream.isUninteresting())
continue;
Location openLocation = stream.getOpenLocation();
if (openLocation == null)
continue;
if (IGNORE_WRAPPED_UNINTERESTING_STREAMS
&& resourceTracker.isUninterestingStreamEscape(stream))
continue;
String sourceFile = javaClass.getSourceFileName();
String leakClass = stream.getStreamBase();
if (isMainMethod(method) && (leakClass.contains("InputStream") || leakClass.contains("Reader")) )
return;
bugAccumulator.accumulateBug(new BugInstance(this, pos.bugType, pos.priority)
.addClassAndMethod(methodGen, sourceFile)
.addTypeOfNamedClass(leakClass).describe(TypeAnnotation.CLOSEIT_ROLE),
SourceLineAnnotation.fromVisitedInstruction(classContext, methodGen, sourceFile, stream.getLocation().getHandle()));
}
}
@Override
public void inspectResult(ClassContext classContext, MethodGen methodGen, CFG cfg,
Dataflow<ResourceValueFrame, ResourceValueAnalysis<Stream>> dataflow, Stream stream) {
ResourceValueFrame exitFrame = dataflow.getResultFact(cfg.getExit());
int exitStatus = exitFrame.getStatus();
if (exitStatus == ResourceValueFrame.OPEN
|| exitStatus == ResourceValueFrame.OPEN_ON_EXCEPTION_PATH) {
// FIXME: Stream object should be queried for the
// priority.
String bugType = stream.getBugType();
int priority = NORMAL_PRIORITY;
if (exitStatus == ResourceValueFrame.OPEN_ON_EXCEPTION_PATH) {
bugType += "_EXCEPTION_PATH";
priority = LOW_PRIORITY;
}
potentialOpenStreamList.add(new PotentialOpenStream(bugType, priority, stream));
} else if (exitStatus == ResourceValueFrame.CLOSED) {
// Remember that this stream was closed on all paths.
// Later, we will mark all of the streams in its equivalence class
// as having been closed.
stream.setClosed();
}
}
// public static void main(String[] argv) throws Exception {
// if (argv.length != 3) {
// System.err.println("Usage: " + FindOpenStream.class.getName() +
// " <class file> <method name> <bytecode offset>");
// System.exit(1);
// }
//
// String classFile = argv[0];
// String methodName = argv[1];
// int offset = Integer.parseInt(argv[2]);
//
// ResourceValueAnalysisTestDriver<Stream, StreamResourceTracker> driver =
// new ResourceValueAnalysisTestDriver<Stream, StreamResourceTracker>() {
// @Override
// public StreamResourceTracker createResourceTracker(ClassContext classContext, Method method) {
// return new StreamResourceTracker(streamFactoryList, classContext.getLookupFailureCallback());
// }
// };
//
// driver.execute(classFile, methodName, offset);
// }
}
// vim:ts=3