/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd.lang.java.typeresolution.rules; import java.util.HashSet; import java.util.List; import java.util.Set; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.lang.java.ast.ASTBlock; import net.sourceforge.pmd.lang.java.ast.ASTBlockStatement; import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceType; import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit; import net.sourceforge.pmd.lang.java.ast.ASTExtendsList; import net.sourceforge.pmd.lang.java.ast.ASTFormalParameters; import net.sourceforge.pmd.lang.java.ast.ASTImplementsList; import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclarator; import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule; /** * The method clone() should only be implemented if the class implements the * Cloneable interface with the exception of a final method that only throws * CloneNotSupportedException. This version uses PMD's type resolution * facilities, and can detect if the class implements or extends a Cloneable * class * * @author acaplan */ public class CloneMethodMustImplementCloneable extends AbstractJavaRule { @Override public Object visit(final ASTClassOrInterfaceDeclaration node, final Object data) { if (extendsOrImplementsCloneable(node)) { return data; } return super.visit(node, data); } private boolean extendsOrImplementsCloneable(final ASTClassOrInterfaceDeclaration node) { if (node.getType() != null) { return Cloneable.class.isAssignableFrom(node.getType()); } // From this point on, this is a best effort, the auxclasspath is incomplete. // TODO : Should we really care about this? // Shouldn't the type resolver / symbol table report missing classes and the user // know results are dependent on running under proper arguments? final ASTImplementsList impl = node.getFirstChildOfType(ASTImplementsList.class); if (impl != null) { for (int ix = 0; ix < impl.jjtGetNumChildren(); ix++) { final Node child = impl.jjtGetChild(ix); if (child.getClass() != ASTClassOrInterfaceType.class) { continue; } final ASTClassOrInterfaceType type = (ASTClassOrInterfaceType) child; if (type.getType() == null) { if ("Cloneable".equals(type.getImage())) { return true; } } else if (Cloneable.class.isAssignableFrom(type.getType())) { return true; } } } if (node.jjtGetNumChildren() != 0 && node.jjtGetChild(0) instanceof ASTExtendsList) { final ASTClassOrInterfaceType type = (ASTClassOrInterfaceType) node.jjtGetChild(0).jjtGetChild(0); final Class<?> clazz = type.getType(); if (clazz != null) { return Cloneable.class.isAssignableFrom(clazz); } } return false; } @Override public Object visit(final ASTMethodDeclaration node, final Object data) { // Is this a clone method? final ASTMethodDeclarator methodDeclarator = node.getFirstChildOfType(ASTMethodDeclarator.class); if (!isCloneMethod(methodDeclarator)) { return data; } // Is the clone method just throwing CloneNotSupportedException? final ASTClassOrInterfaceDeclaration classOrInterface = node.getFirstParentOfType(ASTClassOrInterfaceDeclaration.class); if (classOrInterface != null && //Don't analyze enums, which cannot subclass clone() (node.isFinal() || classOrInterface.isFinal())) { if (node.findDescendantsOfType(ASTBlock.class).size() == 1) { final List<ASTBlockStatement> blocks = node.findDescendantsOfType(ASTBlockStatement.class); if (blocks.size() == 1) { final ASTBlockStatement block = blocks.get(0); final ASTClassOrInterfaceType type = block.getFirstDescendantOfType(ASTClassOrInterfaceType.class); if (type != null && type.getType() != null && type.getNthParent(9).equals(node) && type.getType().equals(CloneNotSupportedException.class)) { return data; } else if (type != null && type.getType() == null && "CloneNotSupportedException".equals(type.getImage())) { return data; } } } } // TODO : Should we really care about this? It can only happen with an incomplete auxclasspath if (classOrInterface != null && classOrInterface.getType() == null) { // Now check other whether implemented or extended classes are defined inside the same file final Set<String> classesNames = determineTopLevelCloneableClasses(classOrInterface); final ASTImplementsList implementsList = classOrInterface.getFirstChildOfType(ASTImplementsList.class); if (implementsList != null) { final List<ASTClassOrInterfaceType> types = implementsList.findChildrenOfType(ASTClassOrInterfaceType.class); for (final ASTClassOrInterfaceType t : types) { if (classesNames.contains(t.getImage())) { return data; } } } final ASTExtendsList extendsList = classOrInterface.getFirstChildOfType(ASTExtendsList.class); if (extendsList != null) { final ASTClassOrInterfaceType type = extendsList.getFirstChildOfType(ASTClassOrInterfaceType.class); if (classesNames.contains(type.getImage())) { return data; } } } // Nothing can save us now addViolation(data, node); return data; } /** * Determines all the class/interface declarations inside this compilation * unit, which implement Cloneable * * @param currentClass * the node of the class, that is currently analyzed (inside this * compilation unit) * @return a Set of class/interface names */ private Set<String> determineTopLevelCloneableClasses(final ASTClassOrInterfaceDeclaration currentClass) { final List<ASTClassOrInterfaceDeclaration> classes = currentClass.getFirstParentOfType(ASTCompilationUnit.class) .findDescendantsOfType(ASTClassOrInterfaceDeclaration.class); final Set<String> classesNames = new HashSet<String>(); for (final ASTClassOrInterfaceDeclaration c : classes) { if (c != currentClass && extendsOrImplementsCloneable(c)) { classesNames.add(c.getImage()); } } return classesNames; } public boolean isCloneMethod(final ASTMethodDeclarator node) { if (!"clone".equals(node.getImage())) { return false; } final int countParams = ((ASTFormalParameters) node.jjtGetChild(0)).jjtGetNumChildren(); if (countParams != 0) { return false; } return true; } }