/** * BSD-style license; for more info see http://pmd.sourceforge.net/license.html */ package net.sourceforge.pmd.lang.java.rule.imports; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.Set; 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.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 List<ASTImportDeclaration> matches = new ArrayList<>(); public UnnecessaryFullyQualifiedNameRule() { super.addRuleChainVisit(ASTCompilationUnit.class); super.addRuleChainVisit(ASTImportDeclaration.class); super.addRuleChainVisit(ASTClassOrInterfaceType.class); super.addRuleChainVisit(ASTName.class); } @Override public Object visit(ASTCompilationUnit node, Object data) { imports.clear(); return data; } @Override public Object visit(ASTImportDeclaration node, Object data) { imports.add(node); return data; } @Override public Object visit(ASTClassOrInterfaceType node, Object data) { checkImports(node, data); return data; } @Override public Object visit(ASTName node, Object data) { if (!(node.jjtGetParent() instanceof ASTImportDeclaration) && !(node.jjtGetParent() instanceof ASTPackageDeclaration)) { checkImports(node, data); } return data; } private void checkImports(JavaNode node, Object data) { String name = node.getImage(); matches.clear(); // Find all "matching" import declarations for (ASTImportDeclaration importDeclaration : imports) { if (importDeclaration.isImportOnDemand()) { // On demand import exactly matches the package of the type if (name.startsWith(importDeclaration.getImportedName())) { if (name.lastIndexOf('.') == importDeclaration.getImportedName().length()) { matches.add(importDeclaration); continue; } } } else { // Exact match of imported class if (name.equals(importDeclaration.getImportedName())) { matches.add(importDeclaration); continue; } // Match of static method call on imported class if (name.startsWith(importDeclaration.getImportedName())) { if (name.lastIndexOf('.') == importDeclaration.getImportedName().length()) { matches.add(importDeclaration); continue; } } } } // 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 // } if (matches.isEmpty() && name.indexOf('.') >= 0) { for (ASTImportDeclaration importDeclaration : imports) { if (importDeclaration.isStatic()) { String[] importParts = importDeclaration.getImportedName().split("\\."); String[] nameParts = name.split("\\."); 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? if (nameParts[nameParts.length - 1].equals(importParts[importParts.length - 1]) && nameParts[nameParts.length - 2].equals(importParts[importParts.length - 2])) { matches.add(importDeclaration); } } } } } if (!matches.isEmpty()) { ASTImportDeclaration firstMatch = matches.get(0); // 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 }); } } matches.clear(); } private boolean isAvoidingConflict(final JavaNode node, final String name, final ASTImportDeclaration firstMatch) { // is it a conflict between different imports? if (firstMatch.isImportOnDemand() && firstMatch.isStatic() && name.indexOf('.') != -1) { final String methodCalled = name.substring(name.indexOf('.') + 1); // Is there any other static import conflictive? for (final ASTImportDeclaration importDeclaration : imports) { if (importDeclaration != firstMatch && importDeclaration.isStatic()) { if (importDeclaration.getImportedName().startsWith(firstMatch.getImportedName()) && importDeclaration.getImportedName().lastIndexOf('.') == firstMatch.getImportedName() .length()) { // 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; } } } } // Is it a conflict with a class in the same file? final String unqualifiedName = name.substring(name.lastIndexOf('.') + 1); final int unqualifiedNameLength = unqualifiedName.length(); 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; } }