/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd.lang.java.rule.design; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.lang.java.ast.ASTArgumentList; import net.sourceforge.pmd.lang.java.ast.ASTArguments; import net.sourceforge.pmd.lang.java.ast.ASTBooleanLiteral; import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit; import net.sourceforge.pmd.lang.java.ast.ASTConstructorDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTEnumDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTExplicitConstructorInvocation; import net.sourceforge.pmd.lang.java.ast.ASTFormalParameter; import net.sourceforge.pmd.lang.java.ast.ASTLiteral; import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclarator; import net.sourceforge.pmd.lang.java.ast.ASTName; import net.sourceforge.pmd.lang.java.ast.ASTPrimaryExpression; import net.sourceforge.pmd.lang.java.ast.ASTPrimaryPrefix; import net.sourceforge.pmd.lang.java.ast.ASTPrimarySuffix; import net.sourceforge.pmd.lang.java.ast.ASTPrimitiveType; import net.sourceforge.pmd.lang.java.ast.ASTReferenceType; import net.sourceforge.pmd.lang.java.ast.ASTType; import net.sourceforge.pmd.lang.java.ast.AccessNode; import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule; /** * Searches through all methods and constructors called from constructors. It * marks as dangerous any call to overridable methods from non-private * constructors. It marks as dangerous any calls to dangerous private * constructors from non-private constructors. * * @author CL Gilbert (dnoyeb@users.sourceforge.net) * * TODO match parameter types. Aggressively strips off any package * names. Normal compares the names as is. TODO What about interface * declarations which can have internal classes */ public final class ConstructorCallsOverridableMethodRule extends AbstractJavaRule { /** * 2: method(); * ASTPrimaryPrefix * ASTName image = "method" * ASTPrimarySuffix * *ASTArguments * 3: a.method(); * ASTPrimaryPrefix -> * ASTName image = "a.method" ??? * ASTPrimarySuffix -> () * ASTArguments * 3: this.method(); * ASTPrimaryPrefix -> this image=null * ASTPrimarySuffix -> method * ASTPrimarySuffix -> () * ASTArguments * <p/> * super.method(); * ASTPrimaryPrefix -> image = "method" * ASTPrimarySuffix -> image = null * ASTArguments -> * <p/> * super.a.method(); * ASTPrimaryPrefix -> image = "a" * ASTPrimarySuffix -> image = "method" * ASTPrimarySuffix -> image = null * ASTArguments -> * <p/> * <p/> * 4: this.a.method(); * ASTPrimaryPrefix -> image = null * ASTPrimarySuffix -> image = "a" * ASTPrimarySuffix -> image = "method" * ASTPrimarySuffix -> * ASTArguments * <p/> * 4: ClassName.this.method(); * ASTPrimaryPrefix * ASTName image = "ClassName" * ASTPrimarySuffix -> this image=null * ASTPrimarySuffix -> image = "method" * ASTPrimarySuffix -> () * ASTArguments * 5: ClassName.this.a.method(); * ASTPrimaryPrefix * ASTName image = "ClassName" * ASTPrimarySuffix -> this image=null * ASTPrimarySuffix -> image="a" * ASTPrimarySuffix -> image="method" * ASTPrimarySuffix -> () * ASTArguments * 5: Package.ClassName.this.method(); * ASTPrimaryPrefix * ASTName image ="Package.ClassName" * ASTPrimarySuffix -> this image=null * ASTPrimarySuffix -> image="method" * ASTPrimarySuffix -> () * ASTArguments * 6: Package.ClassName.this.a.method(); * ASTPrimaryPrefix * ASTName image ="Package.ClassName" * ASTPrimarySuffix -> this image=null * ASTPrimarySuffix -> a * ASTPrimarySuffix -> method * ASTPrimarySuffix -> () * ASTArguments * 5: OuterClass.InnerClass.this.method(); * ASTPrimaryPrefix * ASTName image = "OuterClass.InnerClass" * ASTPrimarySuffix -> this image=null * ASTPrimarySuffix -> method * ASTPrimarySuffix -> () * ASTArguments * 6: OuterClass.InnerClass.this.a.method(); * ASTPrimaryPrefix * ASTName image = "OuterClass.InnerClass" * ASTPrimarySuffix -> this image=null * ASTPrimarySuffix -> a * ASTPrimarySuffix -> method * ASTPrimarySuffix -> () * ASTArguments * <p/> * OuterClass.InnerClass.this.a.method().method().method(); * ASTPrimaryPrefix * ASTName image = "OuterClass.InnerClass" * ASTPrimarySuffix -> this image=null * ASTPrimarySuffix -> a image='a' * ASTPrimarySuffix -> method image='method' * ASTPrimarySuffix -> () image=null * ASTArguments * ASTPrimarySuffix -> method image='method' * ASTPrimarySuffix -> () image=null * ASTArguments * ASTPrimarySuffix -> method image='method' * ASTPrimarySuffix -> () image=null * ASTArguments * <p/> * 3..n: Class.InnerClass[0].InnerClass[n].this.method(); * ASTPrimaryPrefix * ASTName image = "Class[0]..InnerClass[n]" * ASTPrimarySuffix -> image=null * ASTPrimarySuffix -> method * ASTPrimarySuffix -> () * ASTArguments * <p/> * super.aMethod(); * ASTPrimaryPrefix -> aMethod * ASTPrimarySuffix -> () * <p/> * Evaluate right to left */ private static final NullEvalPackage NULL_EVAL_PACKAGE = new NullEvalPackage(); /** * 1 package per class. */ // could use java.util.Stack private final List<EvalPackage> evalPackages = new ArrayList<>(); private static class MethodInvocation { private String name; private ASTPrimaryExpression ape; private List<String> referenceNames; private List<String> qualifierNames; private int argumentSize; private List<String> argumentTypes; private boolean superCall; private MethodInvocation(ASTPrimaryExpression ape, List<String> qualifierNames, List<String> referenceNames, String name, int argumentSize, List<String> argumentTypes, boolean superCall) { this.ape = ape; this.qualifierNames = qualifierNames; this.referenceNames = referenceNames; this.name = name; this.argumentSize = argumentSize; this.argumentTypes = argumentTypes; this.superCall = superCall; } public boolean isSuper() { return superCall; } public String getName() { return name; } public int getArgumentCount() { return argumentSize; } public List<String> getArgumentTypes() { return argumentTypes; } public List<String> getReferenceNames() { return referenceNames; // new ArrayList(variableNames); } public List<String> getQualifierNames() { return qualifierNames; } public ASTPrimaryExpression getASTPrimaryExpression() { return ape; } public static MethodInvocation getMethod(ASTPrimaryExpression node) { MethodInvocation meth = null; int i = node.jjtGetNumChildren(); if (i > 1) { // should always be at least 2, probably can eliminate this check // start at end which is guaranteed, work backwards Node lastNode = node.jjtGetChild(i - 1); // could be ASTExpression for instance 'a[4] = 5'; if (lastNode.jjtGetNumChildren() == 1 && lastNode.jjtGetChild(0) instanceof ASTArguments) { // start putting method together // System.out.println("Putting method together now"); List<String> varNames = new ArrayList<>(); // look in JLS for better name here List<String> packagesAndClasses = new ArrayList<>(); String methodName = null; ASTArguments args = (ASTArguments) lastNode.jjtGetChild(0); int numOfArguments = args.getArgumentCount(); List<String> argumentTypes = ConstructorCallsOverridableMethodRule.getArgumentTypes(args); boolean superFirst = false; int thisIndex = -1; FIND_SUPER_OR_THIS: { // search all nodes except last for 'this' or 'super'. // will be at: (node 0 | node 1 | nowhere) // this is an ASTPrimarySuffix with a null image and // does not have child (which will be of type // ASTArguments) // this is an ASTPrimaryPrefix with a null image and an // ASTName that has a null image // super is an ASTPrimarySuffix with a null image and // does not have child (which will be of type // ASTArguments) // super is an ASTPrimaryPrefix with a non-null image for (int x = 0; x < i - 1; x++) { Node child = node.jjtGetChild(x); // check suffix type match if (child instanceof ASTPrimarySuffix) { ASTPrimarySuffix child2 = (ASTPrimarySuffix) child; // String name = // getNameFromSuffix((ASTPrimarySuffix)child); // System.out.println("found name suffix of : " // + name); if (child2.getImage() == null && child2.jjtGetNumChildren() == 0) { thisIndex = x; break; } // could be super, could be this. currently we // cant tell difference so we miss super when // XYZ.ClassName.super.method(); // still works though. } else if (child instanceof ASTPrimaryPrefix) { // check prefix type match ASTPrimaryPrefix child2 = (ASTPrimaryPrefix) child; if (getNameFromPrefix(child2) == null) { if (child2.getImage() == null) { thisIndex = x; break; } else { // happens when super is used // [super.method(): image = 'method'] superFirst = true; thisIndex = x; // the true super is at an unusable // index because super.method() has only // 2 nodes [method=0,()=1] // as opposed to the 3 you might expect // and which this.method() actually has. // [this=0,method=1.()=2] break; } } } // else{ // System.err.println("Bad Format error"); //throw // exception, quit evaluating this compilation node // } } } if (thisIndex != -1) { // System.out.println("Found this or super: " + // thisIndex); // Hack that must be removed if and when the patters of // super.method() begins to logically match the rest of // the patterns !!! if (superFirst) { // this is when super is the first // node of statement. no qualifiers, // all variables or method // System.out.println("super first"); FIRSTNODE: { ASTPrimaryPrefix child = (ASTPrimaryPrefix) node.jjtGetChild(0); String name = child.getImage(); // special case if (i == 2) { // last named node = method name methodName = name; } else { // not the last named node so its only // var name varNames.add(name); } } OTHERNODES: { // variables for (int x = 1; x < i - 1; x++) { Node child = node.jjtGetChild(x); ASTPrimarySuffix ps = (ASTPrimarySuffix) child; if (!ps.isArguments()) { String name = ((ASTPrimarySuffix) child).getImage(); if (x == i - 2) { // last node methodName = name; } else { // not the last named node so // its only var name varNames.add(name); } } } } } else { // not super call FIRSTNODE: { if (thisIndex == 1) { // qualifiers in node 0 ASTPrimaryPrefix child = (ASTPrimaryPrefix) node.jjtGetChild(0); String toParse = getNameFromPrefix(child); // System.out.println("parsing for // class/package names in : " + toParse); java.util.StringTokenizer st = new java.util.StringTokenizer(toParse, "."); while (st.hasMoreTokens()) { packagesAndClasses.add(st.nextToken()); } } } OTHERNODES: { // other methods called in this // statement are grabbed here // this is at 0, then no Qualifiers // this is at 1, the node 0 contains qualifiers for (int x = thisIndex + 1; x < i - 1; x++) { // everything after this is var name or method name ASTPrimarySuffix child = (ASTPrimarySuffix) node.jjtGetChild(x); if (!child.isArguments()) { // skip the () of method calls String name = child.getImage(); // System.out.println("Found suffix: " + // suffixName); if (x == i - 2) { methodName = name; } else { varNames.add(name); } } } } } } else { // if no this or super found, everything is method // name or variable // System.out.println("no this found:"); FIRSTNODE: { // variable names are in the prefix + the // first method call [a.b.c.x()] ASTPrimaryPrefix child = (ASTPrimaryPrefix) node.jjtGetChild(0); String toParse = getNameFromPrefix(child); // System.out.println("parsing for var names in : " // + toParse); java.util.StringTokenizer st = new java.util.StringTokenizer(toParse, "."); while (st.hasMoreTokens()) { String value = st.nextToken(); if (!st.hasMoreTokens()) { if (i == 2) { // if this expression is 2 // nodes long, then the last // part of prefix is method // name methodName = value; } else { varNames.add(value); } } else { // variable name varNames.add(value); } } } OTHERNODES: { // other methods called in this statement // are grabbed here for (int x = 1; x < i - 1; x++) { ASTPrimarySuffix child = (ASTPrimarySuffix) node.jjtGetChild(x); if (!child.isArguments()) { String name = child.getImage(); if (x == i - 2) { methodName = name; } else { varNames.add(name); } } } } } meth = new MethodInvocation(node, packagesAndClasses, varNames, methodName, numOfArguments, argumentTypes, superFirst); // meth.show(); } } return meth; } public void show() { System.out.println("<MethodInvocation>"); System.out.println(" <Qualifiers>"); for (String name : getQualifierNames()) { System.out.println(" " + name); } System.out.println(" </Qualifiers>"); System.out.println(" <Super>" + isSuper() + "</Super>"); System.out.println(" <References>"); for (String name : getReferenceNames()) { System.out.println(" " + name); } System.out.println(" </References>"); System.out.println(" <Name>" + getName() + "</Name>"); System.out.println(" <ArgumentCount>" + getArgumentCount() + "</ArgumentCount>"); System.out.println(" <ArgumentTypes>" + getArgumentTypes() + "</ArgumentTypes>"); System.out.println("</MethodInvocation>"); } } private static final class ConstructorInvocation { private ASTExplicitConstructorInvocation eci; private String name; private int count = 0; private List<String> argumentTypes = new ArrayList<>(); ConstructorInvocation(ASTExplicitConstructorInvocation eci) { this.eci = eci; List<ASTArguments> l = eci.findChildrenOfType(ASTArguments.class); if (!l.isEmpty()) { ASTArguments aa = l.get(0); count = aa.getArgumentCount(); argumentTypes = ConstructorCallsOverridableMethodRule.getArgumentTypes(aa); } name = eci.getImage(); } public ASTExplicitConstructorInvocation getASTExplicitConstructorInvocation() { return eci; } public int getArgumentCount() { return count; } public List<String> getArgumentTypes() { return argumentTypes; } public String getName() { return name; } } private static final class MethodHolder { private ASTMethodDeclarator amd; private boolean dangerous; private String called; MethodHolder(ASTMethodDeclarator amd) { this.amd = amd; } public void setCalledMethod(String name) { this.called = name; } public String getCalled() { return this.called; } public ASTMethodDeclarator getASTMethodDeclarator() { return amd; } public boolean isDangerous() { return dangerous; } public void setDangerous() { dangerous = true; } } private static final class ConstructorHolder { private ASTConstructorDeclaration cd; private boolean dangerous; private ConstructorInvocation ci; private boolean ciInitialized; ConstructorHolder(ASTConstructorDeclaration cd) { this.cd = cd; } public ASTConstructorDeclaration getASTConstructorDeclaration() { return cd; } public ConstructorInvocation getCalledConstructor() { if (!ciInitialized) { initCI(); } return ci; } public ASTExplicitConstructorInvocation getASTExplicitConstructorInvocation() { ASTExplicitConstructorInvocation eci = null; if (!ciInitialized) { initCI(); } if (ci != null) { eci = ci.getASTExplicitConstructorInvocation(); } return eci; } private void initCI() { // only 1... List<ASTExplicitConstructorInvocation> expressions = cd .findChildrenOfType(ASTExplicitConstructorInvocation.class); if (!expressions.isEmpty()) { ASTExplicitConstructorInvocation eci = expressions.get(0); ci = new ConstructorInvocation(eci); // System.out.println("Const call " + eci.getImage()); //super // or this??? } ciInitialized = true; } public boolean isDangerous() { return dangerous; } public void setDangerous(boolean dangerous) { this.dangerous = dangerous; } } private static int compareNodes(Node n1, Node n2) { int l1 = n1.getBeginLine(); int l2 = n2.getBeginLine(); if (l1 == l2) { return n1.getBeginColumn() - n2.getBeginColumn(); } return l1 - l2; } private static class MethodHolderComparator implements Comparator<MethodHolder> { @Override public int compare(MethodHolder o1, MethodHolder o2) { return compareNodes(o1.getASTMethodDeclarator(), o2.getASTMethodDeclarator()); } } private static class ConstructorHolderComparator implements Comparator<ConstructorHolder> { @Override public int compare(ConstructorHolder o1, ConstructorHolder o2) { return compareNodes(o1.getASTConstructorDeclaration(), o2.getASTConstructorDeclaration()); } } /** * 1 package per class. holds info for evaluating a single class. */ private static class EvalPackage { public String className; public List<MethodInvocation> calledMethods; public Map<MethodHolder, List<MethodInvocation>> allMethodsOfClass; public List<ConstructorInvocation> calledConstructors; public Map<ConstructorHolder, List<MethodInvocation>> allPrivateConstructorsOfClass; EvalPackage() { } EvalPackage(String className) { this.className = className; // meths called from constructor this.calledMethods = new ArrayList<>(); this.allMethodsOfClass = new TreeMap<>(new MethodHolderComparator()); // all constructors called from constructor this.calledConstructors = new ArrayList<>(); this.allPrivateConstructorsOfClass = new TreeMap<>(new ConstructorHolderComparator()); } } private static final class NullEvalPackage extends EvalPackage { NullEvalPackage() { className = ""; calledMethods = Collections.emptyList(); allMethodsOfClass = Collections.emptyMap(); calledConstructors = Collections.emptyList(); allPrivateConstructorsOfClass = Collections.emptyMap(); } } private EvalPackage getCurrentEvalPackage() { return evalPackages.get(evalPackages.size() - 1); } /** * Adds and evaluation package and makes it current */ private void putEvalPackage(EvalPackage ep) { evalPackages.add(ep); } private void removeCurrentEvalPackage() { evalPackages.remove(evalPackages.size() - 1); } private void clearEvalPackages() { evalPackages.clear(); } /** * This check must be evaluated independently for each class. Inner classes * get their own EvalPackage in order to perform independent evaluation. */ private Object visitClassDec(ASTClassOrInterfaceDeclaration node, Object data) { String className = node.getImage(); if (!node.isFinal()) { putEvalPackage(new EvalPackage(className)); } else { putEvalPackage(NULL_EVAL_PACKAGE); } // store any errors caught from other passes. super.visit(node, data); // skip this class if it has no evaluation package if (!(getCurrentEvalPackage() instanceof NullEvalPackage)) { // evaluate danger of all methods in class, this method will return // false when all methods have been evaluated while (evaluateDangerOfMethods(getCurrentEvalPackage().allMethodsOfClass)) { } // NOPMD // evaluate danger of constructors evaluateDangerOfConstructors1(getCurrentEvalPackage().allPrivateConstructorsOfClass, getCurrentEvalPackage().allMethodsOfClass.keySet()); while (evaluateDangerOfConstructors2(getCurrentEvalPackage().allPrivateConstructorsOfClass)) { } // NOPMD // get each method called on this object from a non-private // constructor, if its dangerous flag it for (MethodInvocation meth : getCurrentEvalPackage().calledMethods) { // check against each dangerous method in class for (MethodHolder h : getCurrentEvalPackage().allMethodsOfClass.keySet()) { if (h.isDangerous()) { String methName = h.getASTMethodDeclarator().getImage(); int count = h.getASTMethodDeclarator().getParameterCount(); List<String> parameterTypes = getMethodDeclaratorParameterTypes(h.getASTMethodDeclarator()); if (methName.equals(meth.getName()) && meth.getArgumentCount() == count && parameterTypes.equals(meth.getArgumentTypes())) { addViolation(data, meth.getASTPrimaryExpression(), "method '" + h.getCalled() + "'"); } } } } // get each unsafe private constructor, and check if its called from // any non private constructors for (ConstructorHolder ch : getCurrentEvalPackage().allPrivateConstructorsOfClass.keySet()) { if (ch.isDangerous()) { // if its dangerous check if its called // from any non-private constructors // System.out.println("visitClassDec Evaluating dangerous // constructor with " + // ch.getASTConstructorDeclaration().getParameterCount() + " // params"); int paramCount = ch.getASTConstructorDeclaration().getParameterCount(); for (ConstructorInvocation ci : getCurrentEvalPackage().calledConstructors) { if (ci.getArgumentCount() == paramCount) { // match name super / this !? addViolation(data, ci.getASTExplicitConstructorInvocation(), "constructor"); } } } } } // finished evaluating this class, move up a level removeCurrentEvalPackage(); return data; } /** * Check the methods called on this class by each of the methods on this * class. If a method calls an unsafe method, mark the calling method as * unsafe. This changes the list of unsafe methods which necessitates * another pass. Keep passing until you make a clean pass in which no * methods are changed to unsafe. For speed it is possible to limit the * number of passes. * <p/> * Impossible to tell type of arguments to method, so forget method matching * on types. just use name and num of arguments. will be some false hits, * but oh well. * * TODO investigate limiting the number of passes through config. */ private boolean evaluateDangerOfMethods(Map<MethodHolder, List<MethodInvocation>> classMethodMap) { // check each method if it calls overridable method boolean found = false; for (Map.Entry<MethodHolder, List<MethodInvocation>> entry : classMethodMap.entrySet()) { MethodHolder h = entry.getKey(); List<MethodInvocation> calledMeths = entry.getValue(); for (Iterator<MethodInvocation> calledMethsIter = calledMeths.iterator(); calledMethsIter.hasNext() && !h.isDangerous();) { // if this method matches one of our dangerous methods, mark it // dangerous MethodInvocation meth = calledMethsIter.next(); // System.out.println("Called meth is " + meth); for (MethodHolder h3 : classMethodMap.keySet()) { // need to skip self here h == h3 if (h3.isDangerous()) { String matchMethodName = h3.getASTMethodDeclarator().getImage(); int matchMethodParamCount = h3.getASTMethodDeclarator().getParameterCount(); List<String> parameterTypes = getMethodDeclaratorParameterTypes(h3.getASTMethodDeclarator()); // System.out.println("matching " + matchMethodName + " // to " + meth.getName()); if (matchMethodName.equals(meth.getName()) && matchMethodParamCount == meth.getArgumentCount() && parameterTypes.equals(meth.getArgumentTypes())) { h.setDangerous(); h.setCalledMethod(matchMethodName); found = true; break; } } } } } return found; } /** * marks constructors dangerous if they call any dangerous methods Requires * only a single pass as methods are already marked * * TODO optimize by having methods already evaluated somehow!? */ private void evaluateDangerOfConstructors1(Map<ConstructorHolder, List<MethodInvocation>> classConstructorMap, Set<MethodHolder> evaluatedMethods) { // check each constructor in the class for (Map.Entry<ConstructorHolder, List<MethodInvocation>> entry : classConstructorMap.entrySet()) { ConstructorHolder ch = entry.getKey(); if (!ch.isDangerous()) { // if its not dangerous then evaluate if it // should be // if it calls dangerous method mark it as dangerous List<MethodInvocation> calledMeths = entry.getValue(); // check each method it calls for (Iterator<MethodInvocation> calledMethsIter = calledMeths.iterator(); calledMethsIter.hasNext() && !ch.isDangerous();) { // but thee are diff objects // which represent same thing // but were never evaluated, // they need reevaluation MethodInvocation meth = calledMethsIter.next(); // CCE String methName = meth.getName(); int methArgCount = meth.getArgumentCount(); // check each of the already evaluated methods: need to // optimize this out for (MethodHolder h : evaluatedMethods) { if (h.isDangerous()) { String matchName = h.getASTMethodDeclarator().getImage(); int matchParamCount = h.getASTMethodDeclarator().getParameterCount(); List<String> parameterTypes = getMethodDeclaratorParameterTypes(h.getASTMethodDeclarator()); if (methName.equals(matchName) && methArgCount == matchParamCount && parameterTypes.equals(meth.getArgumentTypes())) { ch.setDangerous(true); // System.out.println("evaluateDangerOfConstructors1 // setting dangerous constructor with " + // ch.getASTConstructorDeclaration().getParameterCount() // + " params"); break; } } } } } } } /** * Constructor map should contain a key for each private constructor, and * maps to a List which contains all called constructors of that key. marks * dangerous if call dangerous private constructor we ignore all non-private * constructors here. That is, the map passed in should not contain any * non-private constructors. we return boolean in order to limit the number * of passes through this method but it seems as if we can forgo that and * just process it till its done. */ private boolean evaluateDangerOfConstructors2(Map<ConstructorHolder, List<MethodInvocation>> classConstructorMap) { boolean found = false; // triggers on danger state change // check each constructor in the class for (ConstructorHolder ch : classConstructorMap.keySet()) { ConstructorInvocation calledC = ch.getCalledConstructor(); if (calledC == null || ch.isDangerous()) { continue; } // if its not dangerous then evaluate if it should be // if it calls dangerous constructor mark it as dangerous int cCount = calledC.getArgumentCount(); for (Iterator<ConstructorHolder> innerConstIter = classConstructorMap.keySet().iterator(); innerConstIter .hasNext() && !ch.isDangerous();) { // forget skipping self because that introduces another // check for each, but only 1 hit ConstructorHolder h2 = innerConstIter.next(); if (h2.isDangerous()) { int matchConstArgCount = h2.getASTConstructorDeclaration().getParameterCount(); List<String> parameterTypes = getMethodDeclaratorParameterTypes(h2.getASTConstructorDeclaration()); if (matchConstArgCount == cCount && parameterTypes.equals(calledC.getArgumentTypes())) { ch.setDangerous(true); found = true; // System.out.println("evaluateDangerOfConstructors2 // setting dangerous constructor with " + // ch.getASTConstructorDeclaration().getParameterCount() // + " params"); } } } } return found; } @Override public Object visit(ASTCompilationUnit node, Object data) { clearEvalPackages(); return super.visit(node, data); } @Override public Object visit(ASTEnumDeclaration node, Object data) { // just skip Enums return data; } /** * This check must be evaluated independently for each class. Inner classes * get their own EvalPackage in order to perform independent evaluation. */ @Override public Object visit(ASTClassOrInterfaceDeclaration node, Object data) { if (!node.isInterface()) { return visitClassDec(node, data); } else { putEvalPackage(NULL_EVAL_PACKAGE); // interface may have inner // classes, possible? if not just // skip whole interface Object o = super.visit(node, data); removeCurrentEvalPackage(); return o; } } /** * Non-private constructor's methods are added to a list for later safety * evaluation. Non-private constructor's calls on private constructors are * added to a list for later safety evaluation. Private constructors are * added to a list so their safety to be called can be later evaluated. * * <p>Note: We are not checking private constructor's calls on non-private * constructors because all non-private constructors will be evaluated for * safety anyway. This means we wont flag a private constructor as unsafe * just because it calls an unsafe public constructor. We want to show only * 1 instance of an error, and this would be 2 instances of the same error.</p> * * <p>TODO eliminate the redundancy</p> */ @Override public Object visit(ASTConstructorDeclaration node, Object data) { if (!(getCurrentEvalPackage() instanceof NullEvalPackage)) { // only evaluate if we have an eval package for this class List<MethodInvocation> calledMethodsOfConstructor = new ArrayList<>(); ConstructorHolder ch = new ConstructorHolder(node); addCalledMethodsOfNode(node, calledMethodsOfConstructor, getCurrentEvalPackage().className); if (!node.isPrivate()) { // these calledMethods are what we will evaluate for being // called badly getCurrentEvalPackage().calledMethods.addAll(calledMethodsOfConstructor); // these called private constructors are what we will evaluate // for being called badly // we add all constructors invoked by non-private constructors // but we are only interested in the private ones. We just can't // tell the difference here ASTExplicitConstructorInvocation eci = ch.getASTExplicitConstructorInvocation(); if (eci != null && eci.isThis()) { getCurrentEvalPackage().calledConstructors.add(ch.getCalledConstructor()); } } else { // add all private constructors to list for later evaluation on // if they are safe to call from another constructor // store this constructorHolder for later evaluation getCurrentEvalPackage().allPrivateConstructorsOfClass.put(ch, calledMethodsOfConstructor); } } return super.visit(node, data); } /** * Create a MethodHolder to hold the method. Store the MethodHolder in the * Map as the key Store each method called by the current method as a List * in the Map as the Object */ @Override public Object visit(ASTMethodDeclarator node, Object data) { if (!(getCurrentEvalPackage() instanceof NullEvalPackage)) { // only evaluate if we have an eval package for this class AccessNode parent = (AccessNode) node.jjtGetParent(); MethodHolder h = new MethodHolder(node); if (!parent.isAbstract() && !parent.isPrivate() && !parent.isStatic() && !parent.isFinal()) { // Skip abstract methods, have a separate rule for that h.setDangerous(); // this method is overridable ASTMethodDeclaration decl = node.getFirstParentOfType(ASTMethodDeclaration.class); h.setCalledMethod(decl.getMethodName()); } List<MethodInvocation> l = new ArrayList<>(); addCalledMethodsOfNode(parent, l, getCurrentEvalPackage().className); getCurrentEvalPackage().allMethodsOfClass.put(h, l); } return super.visit(node, data); } /** * Adds all methods called on this instance from within this Node. */ private static void addCalledMethodsOfNode(Node node, List<MethodInvocation> calledMethods, String className) { List<ASTPrimaryExpression> expressions = new ArrayList<>(); node.findDescendantsOfType(ASTPrimaryExpression.class, expressions, !(node instanceof AccessNode)); addCalledMethodsOfNodeImpl(expressions, calledMethods, className); } private static void addCalledMethodsOfNodeImpl(List<ASTPrimaryExpression> expressions, List<MethodInvocation> calledMethods, String className) { for (ASTPrimaryExpression ape : expressions) { MethodInvocation meth = findMethod(ape, className); if (meth != null) { // System.out.println("Adding call " + methName); calledMethods.add(meth); } } } /** * @return A method call on the class passed in, or null if no method call * is found. TODO Need a better way to match the class and package * name to the actual method being called. */ private static MethodInvocation findMethod(ASTPrimaryExpression node, String className) { if (node.jjtGetNumChildren() > 0 && node.jjtGetChild(0).jjtGetNumChildren() > 0 && node.jjtGetChild(0).jjtGetChild(0) instanceof ASTLiteral) { return null; } MethodInvocation meth = MethodInvocation.getMethod(node); boolean found = false; // if(meth != null){ // meth.show(); // } if (meth != null) { // if it's a call on a variable, or on its superclass ignore it. if (meth.getReferenceNames().isEmpty() && !meth.isSuper()) { // if this list does not contain our class name, then its not // referencing our class // this is a cheezy test... but it errs on the side of less // false hits. List<String> packClass = meth.getQualifierNames(); if (!packClass.isEmpty()) { for (String name : packClass) { if (name.equals(className)) { found = true; break; } } } else { found = true; } } } return found ? meth : null; } /** * ASTPrimaryPrefix has name in child node of ASTName */ private static String getNameFromPrefix(ASTPrimaryPrefix node) { String name = null; // should only be 1 child, if more I need more knowledge if (node.jjtGetNumChildren() == 1) { // safety check Node nnode = node.jjtGetChild(0); if (nnode instanceof ASTName) { // just as easy as null check and it // should be an ASTName anyway name = ((ASTName) nnode).getImage(); } } return name; } private static List<String> getMethodDeclaratorParameterTypes(Node methodOrConstructorDeclarator) { List<ASTFormalParameter> parameters = methodOrConstructorDeclarator .findDescendantsOfType(ASTFormalParameter.class); List<String> parameterTypes = new ArrayList<>(); if (parameters != null) { for (ASTFormalParameter p : parameters) { ASTType type = p.getFirstChildOfType(ASTType.class); if (type.jjtGetChild(0) instanceof ASTPrimitiveType) { parameterTypes.add(type.jjtGetChild(0).getImage()); } else if (type.jjtGetChild(0) instanceof ASTReferenceType) { parameterTypes.add("ref"); } else { parameterTypes.add("<unkown>"); } } } return parameterTypes; } private static List<String> getArgumentTypes(ASTArguments args) { List<String> argumentTypes = new ArrayList<>(); ASTArgumentList argumentList = args.getFirstChildOfType(ASTArgumentList.class); if (argumentList != null) { for (int a = 0; a < argumentList.jjtGetNumChildren(); a++) { Node expression = argumentList.jjtGetChild(a); ASTPrimaryPrefix arg = expression.getFirstDescendantOfType(ASTPrimaryPrefix.class); String type = "<unknown>"; if (arg != null && arg.jjtGetNumChildren() > 0) { if (arg.jjtGetChild(0) instanceof ASTLiteral) { ASTLiteral lit = (ASTLiteral) arg.jjtGetChild(0); if (lit.isCharLiteral()) { type = "char"; } else if (lit.isFloatLiteral()) { type = "float"; } else if (lit.isIntLiteral()) { type = "int"; } else if (lit.isStringLiteral()) { type = "String"; } else if (lit.jjtGetNumChildren() > 0 && lit.jjtGetChild(0) instanceof ASTBooleanLiteral) { type = "boolean"; } else if (lit.isDoubleLiteral()) { type = "double"; } else if (lit.isLongLiteral()) { type = "long"; } } else if (arg.jjtGetChild(0) instanceof ASTName) { // ASTName n = (ASTName)arg.jjtGetChild(0); type = "ref"; } } argumentTypes.add(type); } } return argumentTypes; } }