/** * 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.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import net.sourceforge.pmd.lang.ast.Node; import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceBody; import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceBodyDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTConstructorDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTDoStatement; import net.sourceforge.pmd.lang.java.ast.ASTForStatement; import net.sourceforge.pmd.lang.java.ast.ASTIfStatement; import net.sourceforge.pmd.lang.java.ast.ASTLambdaExpression; import net.sourceforge.pmd.lang.java.ast.ASTMethodDeclaration; import net.sourceforge.pmd.lang.java.ast.ASTTryStatement; import net.sourceforge.pmd.lang.java.ast.ASTVariableInitializer; import net.sourceforge.pmd.lang.java.ast.ASTWhileStatement; import net.sourceforge.pmd.lang.java.ast.AccessNode; import net.sourceforge.pmd.lang.java.rule.AbstractJavaRule; import net.sourceforge.pmd.lang.java.symboltable.JavaNameOccurrence; import net.sourceforge.pmd.lang.java.symboltable.VariableNameDeclaration; import net.sourceforge.pmd.lang.symboltable.NameOccurrence; /** * @author Olander */ public class ImmutableFieldRule extends AbstractJavaRule { private enum FieldImmutabilityType { /** Variable is changed in methods and/or in lambdas */ MUTABLE, /** Variable is not changed outside the constructor. */ IMMUTABLE, /** Variable is only written during declaration, if at all. */ CHECKDECL; } @Override public Object visit(ASTClassOrInterfaceDeclaration node, Object data) { Map<VariableNameDeclaration, List<NameOccurrence>> vars = node.getScope() .getDeclarations(VariableNameDeclaration.class); List<ASTConstructorDeclaration> constructors = findAllConstructors(node); for (Map.Entry<VariableNameDeclaration, List<NameOccurrence>> entry : vars.entrySet()) { VariableNameDeclaration field = entry.getKey(); AccessNode accessNodeParent = field.getAccessNodeParent(); if (accessNodeParent.isStatic() || !accessNodeParent.isPrivate() || accessNodeParent.isFinal() || accessNodeParent.isVolatile()) { continue; } FieldImmutabilityType result = initializedInConstructor(entry.getValue(), new HashSet<>(constructors)); if (result == FieldImmutabilityType.MUTABLE) { continue; } if (result == FieldImmutabilityType.IMMUTABLE || result == FieldImmutabilityType.CHECKDECL && initializedWhenDeclared(field)) { addViolation(data, field.getNode(), field.getImage()); } } return super.visit(node, data); } private boolean initializedWhenDeclared(VariableNameDeclaration field) { return ((Node) field.getAccessNodeParent()).hasDescendantOfType(ASTVariableInitializer.class); } private FieldImmutabilityType initializedInConstructor(List<NameOccurrence> usages, Set<ASTConstructorDeclaration> allConstructors) { FieldImmutabilityType result = FieldImmutabilityType.MUTABLE; int methodInitCount = 0; int lambdaUsage = 0; Set<Node> consSet = new HashSet<>(); for (NameOccurrence occ : usages) { JavaNameOccurrence jocc = (JavaNameOccurrence) occ; if (jocc.isOnLeftHandSide() || jocc.isSelfAssignment()) { Node node = jocc.getLocation(); ASTConstructorDeclaration constructor = node.getFirstParentOfType(ASTConstructorDeclaration.class); if (constructor != null) { if (inLoopOrTry(node)) { continue; } // Check for assigns in if-statements, which can depend on // constructor // args or other runtime knowledge and can be a valid reason // to instantiate // in one constructor only if (node.getFirstParentOfType(ASTIfStatement.class) != null) { methodInitCount++; } if (inAnonymousInnerClass(node)) { methodInitCount++; } else { consSet.add(constructor); } } else { if (node.getFirstParentOfType(ASTMethodDeclaration.class) != null) { methodInitCount++; } else if (node.getFirstParentOfType(ASTLambdaExpression.class) != null) { lambdaUsage++; } } } } if (usages.isEmpty() || methodInitCount == 0 && lambdaUsage == 0 && consSet.isEmpty()) { result = FieldImmutabilityType.CHECKDECL; } else { allConstructors.removeAll(consSet); if (allConstructors.isEmpty() && methodInitCount == 0 && lambdaUsage == 0) { result = FieldImmutabilityType.IMMUTABLE; } } return result; } private boolean inLoopOrTry(Node node) { return node.getFirstParentOfType(ASTTryStatement.class) != null || node.getFirstParentOfType(ASTForStatement.class) != null || node.getFirstParentOfType(ASTWhileStatement.class) != null || node.getFirstParentOfType(ASTDoStatement.class) != null; } private boolean inAnonymousInnerClass(Node node) { ASTClassOrInterfaceBodyDeclaration parent = node.getFirstParentOfType(ASTClassOrInterfaceBodyDeclaration.class); return parent != null && parent.isAnonymousInnerClass(); } private List<ASTConstructorDeclaration> findAllConstructors(ASTClassOrInterfaceDeclaration node) { List<ASTConstructorDeclaration> cons = new ArrayList<>(); node.getFirstChildOfType(ASTClassOrInterfaceBody.class).findDescendantsOfType(ASTConstructorDeclaration.class, cons, false); return cons; } }