/*
* fb-contrib - Auxiliary detectors for Java programs
* Copyright (C) 2005-2017 Dave Brosius
*
* 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 com.mebigfatguy.fbcontrib.detect;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import com.mebigfatguy.fbcontrib.utils.BugType;
import com.mebigfatguy.fbcontrib.utils.SignatureUtils;
import com.mebigfatguy.fbcontrib.utils.UnmodifiableSet;
import com.mebigfatguy.fbcontrib.utils.Values;
import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.BytecodeScanningDetector;
import edu.umd.cs.findbugs.FieldAnnotation;
import edu.umd.cs.findbugs.SourceLineAnnotation;
import edu.umd.cs.findbugs.ba.ClassContext;
/**
* looks for tag libraries that are not recycleable because backing members of taglib attributes are set in areas besides the setter method for the attribute.
*/
public class NonRecycleableTaglibs extends BytecodeScanningDetector {
private static final int MAX_ATTRIBUTE_CODE_LENGTH = 60;
private static final Set<String> tagClasses = UnmodifiableSet.create("javax.servlet.jsp.tagext.TagSupport", "javax.servlet.jsp.tagext.BodyTagSupport");
private static final Set<String> validAttrTypes = UnmodifiableSet.create(Values.SIG_PRIMITIVE_BYTE, Values.SIG_PRIMITIVE_CHAR, Values.SIG_PRIMITIVE_DOUBLE,
Values.SIG_PRIMITIVE_FLOAT, Values.SIG_PRIMITIVE_INT, Values.SIG_PRIMITIVE_LONG, Values.SIG_PRIMITIVE_SHORT, Values.SIG_PRIMITIVE_BOOLEAN,
Values.SIG_JAVA_LANG_STRING, "Ljava/util/Date;");
private final BugReporter bugReporter;
/**
* methodname:methodsig to type of setter methods
*/
private Map<String, String> attributes;
/**
* methodname:methodsig to (fieldname:fieldtype)s
*/
private Map<String, Map<String, SourceLineAnnotation>> methodWrites;
private Map<String, FieldAnnotation> fieldAnnotations;
/**
* constructs a NRTL detector given the reporter to report bugs on.
*
* @param bugReporter
* the sync of bug reports
*/
public NonRecycleableTaglibs(BugReporter bugReporter) {
this.bugReporter = bugReporter;
}
/**
* implements the visitor to look for classes that extend the TagSupport or BodyTagSupport class
*
* @param classContext
* the context object for the currently parsed class
*/
@Override
public void visitClassContext(ClassContext classContext) {
try {
JavaClass cls = classContext.getJavaClass();
JavaClass[] superClasses = cls.getSuperClasses();
for (JavaClass superCls : superClasses) {
if (tagClasses.contains(superCls.getClassName())) {
attributes = getAttributes(cls);
if (!attributes.isEmpty()) {
methodWrites = new HashMap<>();
fieldAnnotations = new HashMap<>();
super.visitClassContext(classContext);
reportBugs();
}
break;
}
}
} catch (ClassNotFoundException cnfe) {
bugReporter.reportMissingClass(cnfe);
} finally {
attributes = null;
methodWrites = null;
fieldAnnotations = null;
}
}
/**
* collect all possible attributes given the name of methods available.
*
* @param cls
* the class to look for setter methods to infer properties
* @return the map of possible attributes/types
*/
private static Map<String, String> getAttributes(JavaClass cls) {
Map<String, String> atts = new HashMap<>();
Method[] methods = cls.getMethods();
for (Method m : methods) {
String name = m.getName();
if (name.startsWith("set") && m.isPublic() && !m.isStatic()) {
String sig = m.getSignature();
List<String> args = SignatureUtils.getParameterSignatures(sig);
if ((args.size() == 1) && Values.SIG_VOID.equals(SignatureUtils.getReturnSignature(sig))) {
String parmSig = args.get(0);
if (validAttrTypes.contains(parmSig)) {
Code code = m.getCode();
if ((code != null) && (code.getCode().length < MAX_ATTRIBUTE_CODE_LENGTH)) {
atts.put(name + ':' + sig, parmSig);
}
}
}
}
}
return atts;
}
/**
* implements the visitor to
*
* @param obj
* the context object for the currently parsed code object
*/
@Override
public void visitCode(Code obj) {
Method m = getMethod();
if (!Values.CONSTRUCTOR.equals(m.getName())) {
super.visitCode(obj);
}
}
/**
* implements the visitor to record storing of fields, and where they occur
*
* @param seen
* the currently parsed opcode
*/
@Override
public void sawOpcode(int seen) {
if (seen == PUTFIELD) {
String methodInfo = getMethodName() + ':' + getMethodSig();
Map<String, SourceLineAnnotation> fields = methodWrites.get(methodInfo);
if (fields == null) {
fields = new HashMap<>();
methodWrites.put(methodInfo, fields);
}
String fieldName = getNameConstantOperand();
String fieldSig = getSigConstantOperand();
FieldAnnotation fa = new FieldAnnotation(getDottedClassName(), fieldName, fieldSig, false);
fieldAnnotations.put(fieldName, fa);
fields.put(fieldName + ':' + fieldSig, SourceLineAnnotation.fromVisitedInstruction(this));
}
}
/**
* generates all the bug reports for attributes that are not recycleable
*/
private void reportBugs() {
for (Map.Entry<String, String> attEntry : attributes.entrySet()) {
String methodInfo = attEntry.getKey();
String attType = attEntry.getValue();
Map<String, SourceLineAnnotation> fields = methodWrites.get(methodInfo);
if ((fields == null) || (fields.size() != 1)) {
continue;
}
String fieldInfo = fields.keySet().iterator().next();
int colonPos = fieldInfo.indexOf(':');
String fieldType = fieldInfo.substring(colonPos + 1);
if (!attType.equals(fieldType)) {
continue;
}
String fieldName = fieldInfo.substring(0, colonPos);
for (Map.Entry<String, Map<String, SourceLineAnnotation>> fwEntry : methodWrites.entrySet()) {
if (fwEntry.getKey().equals(methodInfo)) {
continue;
}
SourceLineAnnotation sla = fwEntry.getValue().get(fieldInfo);
if (sla != null) {
bugReporter.reportBug(new BugInstance(this, BugType.NRTL_NON_RECYCLEABLE_TAG_LIB.name(), NORMAL_PRIORITY).addClass(this)
.addField(fieldAnnotations.get(fieldName)).addSourceLine(sla));
break;
}
}
}
}
}