Java tutorial
/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd.lang.java.rule.codestyle; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; import org.apache.commons.lang3.StringUtils; import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceType; import net.sourceforge.pmd.lang.java.ast.ASTCompilationUnit; import net.sourceforge.pmd.lang.java.ast.ASTImportDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTName; import net.sourceforge.pmd.lang.java.ast.ASTPackageDeclaration; 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.AbstractJavaTypeNode; import net.sourceforge.pmd.lang.java.ast.JavaNode; import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule; import net.sourceforge.pmd.lang.java.symboltable.SourceFileScope; public class UnnecessaryFullyQualifiedNameRule extends AbstractJavaRule { private List<ASTImportDeclaration> imports = new ArrayList<>(); private String currentPackage; public UnnecessaryFullyQualifiedNameRule() { super.addRuleChainVisit(ASTCompilationUnit.class); super.addRuleChainVisit(ASTPackageDeclaration.class); super.addRuleChainVisit(ASTImportDeclaration.class); super.addRuleChainVisit(ASTClassOrInterfaceType.class); super.addRuleChainVisit(ASTName.class); } @Override public Object visit(ASTCompilationUnit node, Object data) { imports.clear(); currentPackage = null; return data; } @Override public Object visit(ASTPackageDeclaration node, Object data) { currentPackage = node.getPackageNameImage(); return data; } @Override public Object visit(ASTImportDeclaration node, Object data) { imports.add(node); return data; } @Override public Object visit(ASTClassOrInterfaceType node, Object data) { // This name has no qualification, it can't be unnecessarily qualified if (node.getImage().indexOf('.') < 0) { return data; } checkImports(node, data); return data; } @Override public Object visit(ASTName node, Object data) { if (!(node.jjtGetParent() instanceof ASTImportDeclaration) && !(node.jjtGetParent() instanceof ASTPackageDeclaration)) { // This name has no qualification, it can't be unnecessarily qualified if (node.getImage().indexOf('.') < 0) { return data; } checkImports(node, data); } return data; } /** * Returns true if the name could be imported by this declaration. * The name must be fully qualified, the import is either on-demand * or static, that is its {@link ASTImportDeclaration#getImportedName()} * is the enclosing package or type name of the imported type or static member. */ private boolean declarationMatches(ASTImportDeclaration decl, String name) { return name.startsWith(decl.getImportedName()) && name.lastIndexOf('.') == decl.getImportedName().length(); } private boolean couldBeMethodCall(JavaNode node) { if (node.getNthParent(2) instanceof ASTPrimaryExpression && node.getNthParent(1) instanceof ASTPrimaryPrefix) { int nextSibling = node.jjtGetParent().jjtGetChildIndex() + 1; if (node.getNthParent(2).jjtGetNumChildren() > nextSibling) { return node.getNthParent(2).jjtGetChild(nextSibling) instanceof ASTPrimarySuffix; } } return false; } private void checkImports(AbstractJavaTypeNode node, Object data) { String name = node.getImage(); List<ASTImportDeclaration> matches = new ArrayList<>(); // Find all "matching" import declarations for (ASTImportDeclaration importDeclaration : imports) { if (!importDeclaration.isImportOnDemand()) { // Exact match of imported class if (name.equals(importDeclaration.getImportedName())) { matches.add(importDeclaration); continue; } } // On demand import exactly matches the package of the type // Or match of static method call on imported class if (declarationMatches(importDeclaration, name)) { matches.add(importDeclaration); } } // If there is no direct match, consider if we match the tail end of a // direct static import, but also a static method on a class import. // For example: // // import java.util.Arrays; // import static java.util.Arrays.asList; // static { // List list1 = Arrays.asList("foo"); // Array class name not needed! // List list2 = asList("foo"); // Preferred, used static import // } // // Or: The usage of a FQN is correct, if there is another import with the same class. // Example // import foo.String; // static { // java.lang.String s = "a"; // } if (matches.isEmpty()) { for (ASTImportDeclaration importDeclaration : imports) { String[] importParts = importDeclaration.getImportedName().split("\\."); String[] nameParts = name.split("\\."); if (importDeclaration.isStatic()) { if (importDeclaration.isImportOnDemand()) { // Name class part matches class part of static import? if (nameParts[nameParts.length - 2].equals(importParts[importParts.length - 1])) { matches.add(importDeclaration); } } else { // Last 2 parts match? Class + Method name if (nameParts[nameParts.length - 1].equals(importParts[importParts.length - 1]) && nameParts[nameParts.length - 2].equals(importParts[importParts.length - 2])) { matches.add(importDeclaration); } } } else if (!importDeclaration.isImportOnDemand()) { // last part matches? if (nameParts[nameParts.length - 1].equals(importParts[importParts.length - 1])) { matches.add(importDeclaration); } else if (couldBeMethodCall(node) && nameParts.length > 1 && nameParts[nameParts.length - 2].equals(importParts[importParts.length - 1])) { // maybe the Name is part of a method call, then the second two last part needs to match matches.add(importDeclaration); } } } } if (matches.isEmpty()) { if (isJavaLangImplicit(node)) { addViolation(data, node, new Object[] { node.getImage(), "java.lang.*", "implicit " }); } else if (isSamePackage(node)) { addViolation(data, node, new Object[] { node.getImage(), currentPackage + ".*", "same package " }); } } else { ASTImportDeclaration firstMatch = findFirstMatch(matches); // Could this done to avoid a conflict? if (!isAvoidingConflict(node, name, firstMatch)) { String importStr = firstMatch.getImportedName() + (firstMatch.isImportOnDemand() ? ".*" : ""); String type = firstMatch.isStatic() ? "static " : ""; addViolation(data, node, new Object[] { node.getImage(), importStr, type }); } } } private ASTImportDeclaration findFirstMatch(List<ASTImportDeclaration> imports) { // first search only static imports ASTImportDeclaration result = null; for (ASTImportDeclaration importDeclaration : imports) { if (importDeclaration.isStatic()) { result = importDeclaration; break; } } // then search all non-static, if needed if (result == null) { for (ASTImportDeclaration importDeclaration : imports) { if (!importDeclaration.isStatic()) { result = importDeclaration; break; } } } return result; } private boolean isSamePackage(AbstractJavaTypeNode node) { String name = node.getImage(); return name.substring(0, name.lastIndexOf('.')).equals(currentPackage); } private boolean isJavaLangImplicit(AbstractJavaTypeNode node) { String name = node.getImage(); boolean isJavaLang = name != null && name.startsWith("java.lang."); if (isJavaLang && node.getType() != null && node.getType().getPackage() != null) { // valid would be ProcessBuilder.Redirect.PIPE but not java.lang.ProcessBuilder.Redirect.PIPE String packageName = node.getType().getPackage() // package might be null, if type is an array type... .getName(); return "java.lang".equals(packageName); } else if (isJavaLang) { // only java.lang.* is implicitly imported, but not e.g. java.lang.reflection.* return StringUtils.countMatches(name, '.') == 2; } return false; } private boolean isAvoidingConflict(final AbstractJavaTypeNode node, final String name, final ASTImportDeclaration firstMatch) { // is it a conflict between different imports? if (firstMatch.isImportOnDemand() && firstMatch.isStatic()) { final String methodCalled = name.substring(name.indexOf('.') + 1); // Is there any other static import conflictive? for (final ASTImportDeclaration importDeclaration : imports) { if (!Objects.equals(importDeclaration, firstMatch) && importDeclaration.isStatic()) { if (declarationMatches(firstMatch, importDeclaration.getImportedName())) { // A conflict against the same class is not an excuse, // ie: // import java.util.Arrays; // import static java.util.Arrays.asList; continue; } if (importDeclaration.isImportOnDemand()) { // We need type resolution to make sure there is a // conflicting method if (importDeclaration.getType() != null) { for (final Method m : importDeclaration.getType().getMethods()) { if (m.getName().equals(methodCalled)) { return true; } } } } else if (importDeclaration.getImportedName().endsWith(methodCalled)) { return true; } } } } final String unqualifiedName = name.substring(name.lastIndexOf('.') + 1); final int unqualifiedNameLength = unqualifiedName.length(); // There could be a conflict between an import on demand and another import, e.g. // import One.*; // import Two.Problem; // Where One.Problem is a legitimate qualification if (firstMatch.isImportOnDemand() && !firstMatch.isStatic()) { for (ASTImportDeclaration importDeclaration : imports) { if (importDeclaration != firstMatch // NOPMD && !importDeclaration.isStatic() && !importDeclaration.isImportOnDemand()) { // Duplicate imports are legal if (!importDeclaration.getPackageName().equals(firstMatch.getPackageName()) && importDeclaration.getImportedSimpleName().equals(unqualifiedName)) { return true; } } } } // There could be a conflict between an import of a class with the same name as the FQN String importName = firstMatch.getImportedName(); String importUnqualified = importName.substring(importName.lastIndexOf('.') + 1); if (!firstMatch.isImportOnDemand() && !firstMatch.isStatic()) { // the package is different, but the unqualified name is same if (!firstMatch.getImportedName().equals(name) && importUnqualified.equals(unqualifiedName)) { return true; } } // There could be a conflict between an import of a class with the same name as the FQN, which // could be a method call: // import x.y.Thread; // valid qualification (node): java.util.Thread.currentThread() if (couldBeMethodCall(node)) { String[] nameParts = name.split("\\."); String fqnName = name.substring(0, name.lastIndexOf('.')); // seems to be a static method call on a different FQN if (!fqnName.equals(importName) && !firstMatch.isStatic() && !firstMatch.isImportOnDemand() && nameParts.length > 1 && nameParts[nameParts.length - 2].equals(importUnqualified)) { return true; } } // Is it a conflict with a class in the same file? final Set<String> qualifiedTypes = node.getScope().getEnclosingScope(SourceFileScope.class) .getQualifiedTypeNames().keySet(); for (final String qualified : qualifiedTypes) { int fullLength = qualified.length(); if (qualified.endsWith(unqualifiedName) && (fullLength == unqualifiedNameLength || qualified.charAt(fullLength - unqualifiedNameLength - 1) == '.')) { return true; } } return false; } }