/*
* Bytecode Analysis Framework
* 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.ba;
import java.util.HashMap;
import java.util.Map;
import org.apache.bcel.Constants;
import org.apache.bcel.Repository;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.ConstantClass;
import org.apache.bcel.classfile.ConstantFieldref;
import org.apache.bcel.classfile.ConstantNameAndType;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.INVOKESTATIC;
import edu.umd.cs.findbugs.SystemProperties;
/**
* Determine which methods are accessors used by inner classes to access fields
* in their enclosing classes. This has been tested with javac from the Sun JDK
* 1.4.x, but will probably not work with other source to bytecode compilers.
*
* <p>
* The instance of InnerClassAccessMap should be retrieved from the
* AnalysisContext.
* </p>
*
* @author David Hovemeyer
* @see InnerClassAccess
*/
public class InnerClassAccessMap {
private static final boolean DEBUG = SystemProperties.getBoolean("icam.debug");
/*
* ----------------------------------------------------------------------
* Fields
* ----------------------------------------------------------------------
*/
/**
* Map of class names to maps of method names to InnerClassAccess objects
* representing access methods.
*/
private Map<String, Map<String, InnerClassAccess>> classToAccessMap;
/*
* ----------------------------------------------------------------------
* Public interface
* ----------------------------------------------------------------------
*/
/**
* Create an instance.
*
* @return a new instance of InnerClassAccessMap
*/
public static InnerClassAccessMap create() {
return new InnerClassAccessMap();
}
/**
* Get the InnerClassAccess in given class with the given method name.
*
* @param className
* the name of the class
* @param methodName
* the name of the access method
* @return the InnerClassAccess object for the method, or null if the method
* doesn't seem to be an inner class access
*/
public InnerClassAccess getInnerClassAccess(String className, String methodName) throws ClassNotFoundException {
Map<String, InnerClassAccess> map = getAccessMapForClass(className);
return map.get(methodName);
}
/**
* Get the inner class access object for given invokestatic instruction.
* Returns null if the called method is not an inner class access.
*
* @param inv
* the invokestatic instruction
* @param cpg
* the ConstantPoolGen for the method
* @return the InnerClassAccess, or null if the call is not an inner class
* access
*/
public InnerClassAccess getInnerClassAccess(INVOKESTATIC inv, ConstantPoolGen cpg) throws ClassNotFoundException {
String methodName = inv.getMethodName(cpg);
if (methodName.startsWith("access$")) {
String className = inv.getClassName(cpg);
return getInnerClassAccess(className, methodName);
}
return null;
}
/**
* Clear the cache.
*/
public void clearCache() {
classToAccessMap.clear();
}
/*
* ----------------------------------------------------------------------
* Implementation
* ----------------------------------------------------------------------
*/
/**
* Constructor.
*/
private InnerClassAccessMap() {
this.classToAccessMap = new HashMap<String, Map<String, InnerClassAccess>>(3);
}
/**
* Convert byte to unsigned int.
*/
private static int toInt(byte b) {
int value = b & 0x7F;
if ((b & 0x80) != 0)
value |= 0x80;
return value;
}
/**
* Get an unsigned 16 bit constant pool index from a byte array.
*/
private static int getIndex(byte[] instructionList, int index) {
return (toInt(instructionList[index + 1]) << 8) | toInt(instructionList[index + 2]);
}
private static class LookupFailure extends RuntimeException {
/**
*
*/
private static final long serialVersionUID = 1L;
private final ClassNotFoundException exception;
public LookupFailure(ClassNotFoundException exception) {
this.exception = exception;
}
public ClassNotFoundException getException() {
return exception;
}
}
/**
* Callback to scan an access method to determine what field it accesses,
* and whether the field is loaded or stored.
*/
private static class InstructionCallback implements BytecodeScanner.Callback {
private JavaClass javaClass;
private String methodName;
private String methodSig;
private byte[] instructionList;
private InnerClassAccess access;
private int accessCount;
/**
* Constructor.
*
* @param javaClass
* the class containing the access method
* @param methodName
* the name of the access method
* @param methodSig
* the signature of the access method
* @param instructionList
* the bytecode of the method
*/
public InstructionCallback(JavaClass javaClass, String methodName, String methodSig, byte[] instructionList) {
this.javaClass = javaClass;
this.methodName = methodName;
this.methodSig = methodSig;
this.instructionList = instructionList;
this.access = null;
this.accessCount = 0;
}
public void handleInstruction(int opcode, int index) {
switch (opcode) {
case Constants.GETFIELD:
case Constants.PUTFIELD:
setField(getIndex(instructionList, index), false, opcode == Constants.GETFIELD);
break;
case Constants.GETSTATIC:
case Constants.PUTSTATIC:
setField(getIndex(instructionList, index), true, opcode == Constants.GETSTATIC);
break;
}
}
/**
* Get the InnerClassAccess object representing the method.
*
* @return the InnerClassAccess, or null if the method was not found to
* be a simple load or store in the expected form
*/
public InnerClassAccess getAccess() {
return access;
}
/**
* Called to indicate that a field load or store was encountered.
*
* @param cpIndex
* the constant pool index of the fieldref
* @param isStatic
* true if it is a static field access
* @param isLoad
* true if the access is a load
*/
private void setField(int cpIndex, boolean isStatic, boolean isLoad) {
// We only allow one field access for an accessor method.
accessCount++;
if (accessCount != 1) {
access = null;
return;
}
ConstantPool cp = javaClass.getConstantPool();
ConstantFieldref fieldref = (ConstantFieldref) cp.getConstant(cpIndex);
ConstantClass cls = (ConstantClass) cp.getConstant(fieldref.getClassIndex());
String className = cls.getBytes(cp).replace('/', '.');
ConstantNameAndType nameAndType = (ConstantNameAndType) cp.getConstant(fieldref.getNameAndTypeIndex());
String fieldName = nameAndType.getName(cp);
String fieldSig = nameAndType.getSignature(cp);
try {
XField xfield = Hierarchy.findXField(className, fieldName, fieldSig, isStatic);
if (xfield != null && xfield.isStatic() == isStatic && isValidAccessMethod(methodSig, xfield, isLoad)) {
access = new InnerClassAccess(methodName, methodSig, xfield, isLoad);
}
} catch (ClassNotFoundException e) {
throw new LookupFailure(e);
}
}
/**
* Determine if the method appears to be an accessor of the expected
* form. This has only been tested with the Sun JDK 1.4 javac
* (definitely) and jikes 1.18 (I think).
*
* @param methodSig
* the method's signature
* @param field
* the field accessed by the method
* @param isLoad
* true if the access is a load
*/
private boolean isValidAccessMethod(String methodSig, XField field, boolean isLoad) {
// Get the method parameters and return type
// (as they appear in the method signature).
int paramsEnd = methodSig.indexOf(')');
if (paramsEnd < 0)
return false;
String methodParams = methodSig.substring(0, paramsEnd + 1);
String methodReturnType = methodSig.substring(paramsEnd + 1);
// Figure out what the expected method parameters should be
String classSig = "L" + javaClass.getClassName().replace('.', '/') + ";";
StringBuilder buf = new StringBuilder();
buf.append('(');
if (!field.isStatic())
buf.append(classSig); // the OuterClass.this reference
if (!isLoad)
buf.append(field.getSignature()); // the value being stored
buf.append(')');
String expectedMethodParams = buf.toString();
// See if params match
if (!methodParams.equals(expectedMethodParams)) {
if (DEBUG) {
System.out.println("In " + javaClass.getClassName() + "." + methodName + " expected params "
+ expectedMethodParams + ", saw " + methodParams);
System.out.println(isLoad ? "LOAD" : "STORE");
}
return false;
}
// Return type can be either the type of the field, or void.
if (!methodReturnType.equals("V") && !methodReturnType.equals(field.getSignature())) {
if (DEBUG) {
System.out.println("In " + javaClass.getClassName() + "." + methodName + " expected return type V or "
+ field.getSignature() + ", saw " + methodReturnType);
System.out.println(isLoad ? "LOAD" : "STORE");
}
return false;
}
return true;
}
}
private static final Map<String, InnerClassAccess> emptyMap = new HashMap<String, InnerClassAccess>();
/**
* Return a map of inner-class member access method names to the fields that
* they access for given class name.
*
* @param className
* the name of the class
* @return map of access method names to the fields they access
*/
private Map<String, InnerClassAccess> getAccessMapForClass(String className) throws ClassNotFoundException {
Map<String, InnerClassAccess> map = classToAccessMap.get(className);
if (map == null) {
map = new HashMap<String, InnerClassAccess>(3);
if (!className.startsWith("[")) {
JavaClass javaClass = Repository.lookupClass(className);
Method[] methodList = javaClass.getMethods();
for (Method method : methodList) {
String methodName = method.getName();
if (!methodName.startsWith("access$"))
continue;
Code code = method.getCode();
if (code == null)
continue;
if (DEBUG)
System.out.println("Analyzing " + className + "." + method.getName()
+ " as an inner-class access method...");
byte[] instructionList = code.getCode();
String methodSig = method.getSignature();
InstructionCallback callback = new InstructionCallback(javaClass, methodName, methodSig, instructionList);
try {
new BytecodeScanner().scan(instructionList, callback);
} catch (LookupFailure lf) {
throw lf.getException();
}
InnerClassAccess access = callback.getAccess();
if (DEBUG)
System.out.println((access != null ? "IS" : "IS NOT") + " an inner-class access method");
if (access != null)
map.put(methodName, access);
}
}
if (map.size() == 0)
map = emptyMap;
else
map = new HashMap<String, InnerClassAccess>(map);
classToAccessMap.put(className, map);
}
return map;
}
}
// vim:ts=4