// ===================================================================== // // Copyright (C) 2012 - 2016, Philip Graf // // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // which accompanies this distribution, and is available at // http://www.eclipse.org/legal/epl-v10.html // // ===================================================================== package ch.acanda.eclipse.pmd.java.resolution.design; import java.util.HashSet; import java.util.List; import java.util.Set; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.Assignment; import org.eclipse.jdt.core.dom.Block; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.Expression; import org.eclipse.jdt.core.dom.FieldAccess; import org.eclipse.jdt.core.dom.FieldDeclaration; import org.eclipse.jdt.core.dom.Modifier; import org.eclipse.jdt.core.dom.Modifier.ModifierKeyword; import org.eclipse.jdt.core.dom.SimpleName; import org.eclipse.jdt.core.dom.ThisExpression; import org.eclipse.jdt.core.dom.VariableDeclarationFragment; import org.eclipse.jdt.core.dom.VariableDeclarationStatement; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.text.Position; import ch.acanda.eclipse.pmd.java.resolution.ASTQuickFix; import ch.acanda.eclipse.pmd.java.resolution.ASTUtil; import ch.acanda.eclipse.pmd.java.resolution.Finders; import ch.acanda.eclipse.pmd.java.resolution.NodeFinder; import ch.acanda.eclipse.pmd.marker.PMDMarker; import ch.acanda.eclipse.pmd.ui.util.PMDPluginImages; import com.google.common.base.Optional; /** * Quick fix for the rule <a href="http://pmd.sourceforge.net/rules/java/design.html#SingularField">SingularField</a>. * It replaces the field with a local variable. * * @author Philip Graf */ public final class SingularFieldQuickFix extends ASTQuickFix<VariableDeclarationFragment> { public SingularFieldQuickFix(final PMDMarker marker) { super(marker); } @Override protected ImageDescriptor getImageDescriptor() { return PMDPluginImages.QUICKFIX_CHANGE; } @Override public String getLabel() { return "Replace with a local variable"; } @Override public String getDescription() { return "Replaces the field with a local variable."; } @Override protected NodeFinder<CompilationUnit, VariableDeclarationFragment> getNodeFinder(final Position position) { return Finders.nodeWithinPosition(getNodeType(), position); } /** * Replaces the field declaration with a local variable declaration. */ @Override protected boolean apply(final VariableDeclarationFragment node) { final String name = node.getName().getIdentifier(); final AssignmentNodeFinder finder = new AssignmentNodeFinder(name); final Optional<Assignment> assignment = finder.findNode(node.getParent().getParent()); if (assignment.isPresent()) { replaceAssignment(node, assignment.get(), !finder.hasMoreThanOneAssignment()); updateFieldDeclaration(node); } return assignment.isPresent(); } /** * Replaces the assignment with a variable declaration. If the assignment is the only one in the block for this * variable, the final modifier is added to the declaration. */ @SuppressWarnings("unchecked") private void replaceAssignment(final VariableDeclarationFragment node, final Assignment assignment, final boolean finalDeclaration) { final FieldDeclaration fieldDeclaration = (FieldDeclaration) node.getParent(); final VariableDeclarationStatement declaration = (VariableDeclarationStatement) node.getAST().createInstance(VariableDeclarationStatement.class); declaration.setType(ASTUtil.copy(fieldDeclaration.getType())); final VariableDeclarationFragment fragment = (VariableDeclarationFragment) node.getAST().createInstance(VariableDeclarationFragment.class); fragment.setName(ASTUtil.copy(node.getName())); fragment.setInitializer(ASTUtil.copy(assignment.getRightHandSide())); declaration.fragments().add(fragment); if (finalDeclaration) { final Modifier modifier = (Modifier) node.getAST().createInstance(Modifier.class); modifier.setKeyword(ModifierKeyword.FINAL_KEYWORD); declaration.modifiers().add(modifier); } ASTUtil.replace(assignment.getParent(), declaration); } /** * Updates the field declaration. If the replaced field was the only fragment, the entire field declaration is * removed. Otherwise the field declaration stays and only the respective fragment is removed. */ private void updateFieldDeclaration(final VariableDeclarationFragment node) { final FieldDeclaration fieldDeclaration = (FieldDeclaration) node.getParent(); @SuppressWarnings("unchecked") final List<VariableDeclarationFragment> fragments = fieldDeclaration.fragments(); if (fragments.size() > 1) { for (final VariableDeclarationFragment fragment : fragments) { if (fragment.getName().getIdentifier().equals(node.getName().getIdentifier())) { fragment.delete(); } } } else { fieldDeclaration.delete(); } } /** * Finds the assignment that is replaced with a local variable declaration. */ private final class AssignmentNodeFinder extends ASTVisitor implements NodeFinder<ASTNode, Assignment> { private final String fieldName; /** * A shadowing block contains a local variable declaration with the same name as the field. Assignments within * such a block are thus not valid search results. When a shadowing variable is found, its containing block is * added to this list and removed when the block ends, i.e. as long as the collection is not empty, the * assignments are not valid search results. */ private final Set<Block> shadowingBlocks = new HashSet<>(); private Optional<Assignment> searchResult = Optional.absent(); private boolean moreThanOneAssignment; AssignmentNodeFinder(final String fieldName) { super(false); this.fieldName = fieldName; } @Override public Optional<Assignment> findNode(final ASTNode node) { node.accept(this); return searchResult; } @Override @SuppressWarnings("unchecked") public boolean visit(final VariableDeclarationStatement node) { final List<VariableDeclarationFragment> fragments = node.fragments(); for (final VariableDeclarationFragment fragment : fragments) { if (fieldName.equals(fragment.getName().getIdentifier())) { // we found a variable that shadows the field shadowingBlocks.add((Block) node.getParent()); } } return false; } @Override public void endVisit(final Block node) { shadowingBlocks.remove(node); } @Override public boolean visit(final Assignment assignment) { if (shadowingBlocks.isEmpty()) { final Expression lhs = assignment.getLeftHandSide(); if (lhs instanceof SimpleName) { checkName(assignment, (SimpleName) lhs); } else if (lhs instanceof FieldAccess && ((FieldAccess) lhs).getExpression() instanceof ThisExpression) { checkName(assignment, ((FieldAccess) lhs).getName()); } } return false; } private void checkName(final Assignment assignment, final SimpleName variableName) { if (fieldName.equals(variableName.getIdentifier())) { if (!searchResult.isPresent()) { searchResult = Optional.of(assignment); } else { moreThanOneAssignment = true; } } } public boolean hasMoreThanOneAssignment() { return moreThanOneAssignment; } } }