| /******************************************************************************* |
| * Copyright (c) 2021 Fabrice TIERCELIN and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Fabrice TIERCELIN - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.jdt.internal.ui.fix; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.eclipse.core.runtime.CoreException; |
| |
| import org.eclipse.text.edits.TextEditGroup; |
| |
| import org.eclipse.jdt.core.ICompilationUnit; |
| import org.eclipse.jdt.core.dom.AST; |
| 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.CompilationUnit; |
| import org.eclipse.jdt.core.dom.Dimension; |
| 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.IExtendedModifier; |
| import org.eclipse.jdt.core.dom.MethodDeclaration; |
| import org.eclipse.jdt.core.dom.Modifier; |
| import org.eclipse.jdt.core.dom.QualifiedName; |
| import org.eclipse.jdt.core.dom.SimpleName; |
| import org.eclipse.jdt.core.dom.SingleVariableDeclaration; |
| import org.eclipse.jdt.core.dom.Statement; |
| import org.eclipse.jdt.core.dom.ThisExpression; |
| import org.eclipse.jdt.core.dom.Type; |
| import org.eclipse.jdt.core.dom.TypeDeclaration; |
| import org.eclipse.jdt.core.dom.VariableDeclarationExpression; |
| import org.eclipse.jdt.core.dom.VariableDeclarationFragment; |
| import org.eclipse.jdt.core.dom.VariableDeclarationStatement; |
| import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; |
| |
| import org.eclipse.jdt.internal.corext.dom.ASTNodes; |
| import org.eclipse.jdt.internal.corext.fix.CleanUpConstants; |
| import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFix; |
| import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFix.CompilationUnitRewriteOperation; |
| import org.eclipse.jdt.internal.corext.fix.LinkedProposalModel; |
| import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite; |
| |
| import org.eclipse.jdt.ui.cleanup.CleanUpRequirements; |
| import org.eclipse.jdt.ui.cleanup.ICleanUpFix; |
| import org.eclipse.jdt.ui.text.java.IProblemLocation; |
| |
| /** |
| * A fix that refactors a field into a local variable if its use is only local: |
| * <ul> |
| * <li>The previous value should not be read,</li> |
| * <li>The field should be private,</li> |
| * <li>The field should not be final,</li> |
| * <li>The field should be primitive,</li> |
| * <li>The field should not have annotations.</li> |
| * </ul> |
| */ |
| public class SingleUsedFieldCleanUp extends AbstractMultiFix { |
| private static final class FieldUseVisitor extends ASTVisitor { |
| private final SimpleName field; |
| private final Set<SimpleName> occurrences= new LinkedHashSet<>(); |
| |
| private FieldUseVisitor(final SimpleName field) { |
| this.field= field; |
| } |
| |
| @Override |
| public boolean visit(final SimpleName aVariable) { |
| if (field != aVariable |
| && ASTNodes.isSameVariable(field, aVariable)) { |
| occurrences.add(aVariable); |
| } |
| |
| return true; |
| } |
| |
| private Set<SimpleName> getOccurrences() { |
| return occurrences; |
| } |
| } |
| |
| public SingleUsedFieldCleanUp() { |
| this(Collections.emptyMap()); |
| } |
| |
| public SingleUsedFieldCleanUp(final Map<String, String> options) { |
| super(options); |
| } |
| |
| @Override |
| public CleanUpRequirements getRequirements() { |
| boolean requireAST= isEnabled(CleanUpConstants.SINGLE_USED_FIELD); |
| return new CleanUpRequirements(requireAST, false, false, null); |
| } |
| |
| @Override |
| public String[] getStepDescriptions() { |
| if (isEnabled(CleanUpConstants.SINGLE_USED_FIELD)) { |
| return new String[] { MultiFixMessages.SingleUsedFieldCleanUp_description }; |
| } |
| |
| return new String[0]; |
| } |
| |
| @Override |
| public String getPreview() { |
| StringBuilder bld= new StringBuilder(); |
| bld.append("public class MyClass {\n"); //$NON-NLS-1$ |
| |
| if (!isEnabled(CleanUpConstants.SINGLE_USED_FIELD)) { |
| bld.append(" private long singleUsedField;\n"); //$NON-NLS-1$ |
| bld.append("\n"); //$NON-NLS-1$ |
| } |
| |
| bld.append(" public void myMethod() {\n"); //$NON-NLS-1$ |
| |
| if (isEnabled(CleanUpConstants.SINGLE_USED_FIELD)) { |
| bld.append(" long singleUsedField = 123;\n"); //$NON-NLS-1$ |
| } else { |
| bld.append(" singleUsedField = 123;\n"); //$NON-NLS-1$ |
| } |
| |
| bld.append(" System.out.println(singleUsedField);\n"); //$NON-NLS-1$ |
| bld.append(" }\n"); //$NON-NLS-1$ |
| bld.append("}\n"); //$NON-NLS-1$ |
| |
| if (isEnabled(CleanUpConstants.SINGLE_USED_FIELD)) { |
| bld.append("\n\n"); //$NON-NLS-1$ |
| } |
| |
| return bld.toString(); |
| } |
| |
| @Override |
| protected ICleanUpFix createFix(final CompilationUnit unit) throws CoreException { |
| if (!isEnabled(CleanUpConstants.SINGLE_USED_FIELD)) { |
| return null; |
| } |
| |
| final List<CompilationUnitRewriteOperation> rewriteOperations= new ArrayList<>(); |
| |
| unit.accept(new ASTVisitor() { |
| @Override |
| public boolean visit(final TypeDeclaration visited) { |
| for (FieldDeclaration field : visited.getFields()) { |
| if (!maybeReplaceFieldByLocalVariable(visited, field)) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| private boolean maybeReplaceFieldByLocalVariable(final TypeDeclaration visited, final FieldDeclaration field) { |
| if (Modifier.isPrivate(field.getModifiers()) |
| && !Modifier.isFinal(field.getModifiers()) |
| && !hasAnnotation(field) |
| && field.getType().isPrimitiveType()) { |
| for (Object object : field.fragments()) { |
| VariableDeclarationFragment fragment= (VariableDeclarationFragment) object; |
| |
| if (!maybeReplaceFragmentByLocalVariable(visited, field, fragment)) { |
| return false; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| private boolean maybeReplaceFragmentByLocalVariable(final TypeDeclaration visited, final FieldDeclaration field, |
| final VariableDeclarationFragment fragment) { |
| if (fragment.getInitializer() != null && !ASTNodes.isPassiveWithoutFallingThrough(fragment.getInitializer())) { |
| return true; |
| } |
| |
| FieldUseVisitor fieldUseVisitor= new FieldUseVisitor(fragment.getName()); |
| visited.getRoot().accept(fieldUseVisitor); |
| Set<SimpleName> occurrences= fieldUseVisitor.getOccurrences(); |
| |
| MethodDeclaration oneMethodDeclaration= null; |
| |
| for (SimpleName occurrence : occurrences) { |
| MethodDeclaration currentMethodDeclaration= ASTNodes.getTypedAncestor(occurrence, MethodDeclaration.class); |
| |
| if (isVariableDeclaration(occurrence) |
| || isExternalField(occurrence) |
| || currentMethodDeclaration == null |
| || oneMethodDeclaration != null && currentMethodDeclaration != oneMethodDeclaration) { |
| return true; |
| } |
| |
| oneMethodDeclaration= currentMethodDeclaration; |
| } |
| |
| if (oneMethodDeclaration == null) { |
| return true; |
| } |
| |
| boolean isReassigned= isAlwaysErased(occurrences); |
| |
| if (isReassigned) { |
| SimpleName reassignment= findReassignment(occurrences); |
| |
| if (reassignment != null) { |
| ASTNode parent= reassignment.getParent(); |
| |
| if (parent instanceof FieldAccess |
| && reassignment.getLocationInParent() == FieldAccess.NAME_PROPERTY |
| && parent.getLocationInParent() == Assignment.LEFT_HAND_SIDE_PROPERTY) { |
| parent= parent.getParent(); |
| } |
| |
| if (parent instanceof Assignment) { |
| rewriteOperations.add(new SingleUsedFieldOperation(field, fragment, reassignment, occurrences)); |
| return false; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| private SimpleName findReassignment(final Set<SimpleName> occurrences) { |
| for (SimpleName reassignment : occurrences) { |
| if (isReassigned(reassignment) && isReassignmentForAll(reassignment, occurrences)) { |
| return reassignment; |
| } |
| } |
| |
| return null; |
| } |
| |
| private boolean isReassignmentForAll(final SimpleName reassignment, final Set<SimpleName> occurrences) { |
| for (SimpleName occurrence : occurrences) { |
| if (reassignment != occurrence) { |
| Statement statement= ASTNodes.getTypedAncestor(occurrence, Statement.class); |
| boolean isReassigned= false; |
| |
| while (statement != null) { |
| Assignment assignment= ASTNodes.asExpression(statement, Assignment.class); |
| |
| if (assignment != null |
| && ASTNodes.hasOperator(assignment, Assignment.Operator.ASSIGN)) { |
| SimpleName field= ASTNodes.getField(assignment.getLeftHandSide()); |
| |
| if (field == reassignment) { |
| isReassigned= true; |
| break; |
| } |
| } |
| |
| statement= ASTNodes.getPreviousStatement(statement); |
| } |
| |
| if (!isReassigned) { |
| return false; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| private boolean isAlwaysErased(final Set<SimpleName> occurrences) { |
| for (SimpleName occurrence : occurrences) { |
| if (!isReassigned(occurrence)) { |
| Statement statement= ASTNodes.getTypedAncestor(occurrence, Statement.class); |
| boolean isReassigned= false; |
| |
| while (statement != null) { |
| statement= ASTNodes.getPreviousStatement(statement); |
| Assignment assignment= ASTNodes.asExpression(statement, Assignment.class); |
| |
| if (assignment != null |
| && ASTNodes.hasOperator(assignment, Assignment.Operator.ASSIGN)) { |
| SimpleName field= ASTNodes.getField(assignment.getLeftHandSide()); |
| |
| if (ASTNodes.areSameVariables(field, occurrence)) { |
| isReassigned= true; |
| break; |
| } |
| } |
| } |
| |
| if (!isReassigned) { |
| return false; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| private boolean isReassigned(final SimpleName occurrence) { |
| Expression expression= occurrence; |
| |
| if (expression.getParent() instanceof FieldAccess) { |
| expression= (FieldAccess) expression.getParent(); |
| } |
| |
| return expression.getParent() instanceof Assignment |
| && expression.getLocationInParent() == Assignment.LEFT_HAND_SIDE_PROPERTY |
| && ASTNodes.hasOperator((Assignment) expression.getParent(), Assignment.Operator.ASSIGN); |
| } |
| |
| private boolean isExternalField(final SimpleName occurrence) { |
| FieldAccess fieldAccess= ASTNodes.as(occurrence, FieldAccess.class); |
| |
| if (fieldAccess != null) { |
| ThisExpression thisExpression= ASTNodes.as(fieldAccess.getExpression(), ThisExpression.class); |
| |
| if (thisExpression == null || thisExpression.getQualifier() != null) { |
| return true; |
| } |
| } |
| |
| return ASTNodes.is(occurrence, QualifiedName.class); |
| } |
| |
| private boolean isVariableDeclaration(final SimpleName occurrence) { |
| switch (occurrence.getParent().getNodeType()) { |
| case ASTNode.SINGLE_VARIABLE_DECLARATION: |
| case ASTNode.VARIABLE_DECLARATION_STATEMENT: |
| return occurrence.getLocationInParent() == SingleVariableDeclaration.NAME_PROPERTY; |
| |
| case ASTNode.VARIABLE_DECLARATION_EXPRESSION: |
| return occurrence.getLocationInParent() == VariableDeclarationExpression.FRAGMENTS_PROPERTY; |
| |
| case ASTNode.VARIABLE_DECLARATION_FRAGMENT: |
| return occurrence.getLocationInParent() == VariableDeclarationFragment.NAME_PROPERTY; |
| |
| default: |
| return false; |
| } |
| } |
| |
| private boolean hasAnnotation(final FieldDeclaration field) { |
| List<IExtendedModifier> modifiers= field.modifiers(); |
| return modifiers.stream().anyMatch(IExtendedModifier::isAnnotation); |
| } |
| }); |
| |
| if (rewriteOperations.isEmpty()) { |
| return null; |
| } |
| |
| return new CompilationUnitRewriteOperationsFix(MultiFixMessages.SingleUsedFieldCleanUp_description, unit, |
| rewriteOperations.toArray(new CompilationUnitRewriteOperation[0])); |
| } |
| |
| @Override |
| public boolean canFix(final ICompilationUnit compilationUnit, final IProblemLocation problem) { |
| return false; |
| } |
| |
| @Override |
| protected ICleanUpFix createFix(final CompilationUnit unit, final IProblemLocation[] problems) throws CoreException { |
| return null; |
| } |
| |
| private static class SingleUsedFieldOperation extends CompilationUnitRewriteOperation { |
| private final FieldDeclaration field; |
| private final VariableDeclarationFragment fragment; |
| private final SimpleName reassignment; |
| private final Set<SimpleName> occurrences; |
| |
| public SingleUsedFieldOperation(final FieldDeclaration field, final VariableDeclarationFragment fragment, final SimpleName reassignment, Set<SimpleName> occurrences) { |
| this.field= field; |
| this.fragment= fragment; |
| this.reassignment= reassignment; |
| this.occurrences= occurrences; |
| } |
| |
| @Override |
| public void rewriteAST(final CompilationUnitRewrite cuRewrite, final LinkedProposalModel linkedModel) throws CoreException { |
| ASTRewrite rewrite= cuRewrite.getASTRewrite(); |
| AST ast= cuRewrite.getRoot().getAST(); |
| TextEditGroup groupOldFieldDeclaration= createTextEditGroup(MultiFixMessages.SingleUsedFieldCleanUp_description_old_field_declaration, cuRewrite); |
| TextEditGroup groupNewLocalVar= createTextEditGroup(MultiFixMessages.SingleUsedFieldCleanUp_description_new_local_var_declaration, cuRewrite); |
| TextEditGroup groupUsesOfTheVar= createTextEditGroup(MultiFixMessages.SingleUsedFieldCleanUp_description_uses_of_the_var, cuRewrite); |
| |
| boolean isFieldKept= field.fragments().size() != 1; |
| |
| if (isFieldKept) { |
| rewrite.remove(fragment, groupOldFieldDeclaration); |
| ASTNodes.replaceButKeepComment(rewrite, field.getType(), rewrite.createCopyTarget(field.getType()), groupOldFieldDeclaration); |
| } else { |
| rewrite.remove(field, groupOldFieldDeclaration); |
| } |
| |
| Assignment reassignmentAssignment= ASTNodes.getTypedAncestor(reassignment, Assignment.class); |
| |
| VariableDeclarationFragment newFragment= ast.newVariableDeclarationFragment(); |
| newFragment.setName(ASTNodes.createMoveTarget(rewrite, reassignment)); |
| newFragment.setInitializer(ASTNodes.createMoveTarget(rewrite, reassignmentAssignment.getRightHandSide())); |
| List<Dimension> extraDimensions= fragment.extraDimensions(); |
| List<Dimension> newExtraDimensions= newFragment.extraDimensions(); |
| newExtraDimensions.addAll(ASTNodes.createMoveTarget(rewrite, extraDimensions)); |
| |
| VariableDeclarationStatement newDeclareStatement= ast.newVariableDeclarationStatement(newFragment); |
| newDeclareStatement.setType(isFieldKept ? ASTNodes.createMoveTarget(rewrite, field.getType()) : (Type) rewrite.createCopyTarget(field.getType())); |
| List<IExtendedModifier> modifiers= field.modifiers(); |
| List<IExtendedModifier> newModifiers= newDeclareStatement.modifiers(); |
| |
| for (IExtendedModifier iExtendedModifier : modifiers) { |
| Modifier modifier= (Modifier) iExtendedModifier; |
| |
| if (!modifier.isPrivate() && !modifier.isStatic()) { |
| newModifiers.add(isFieldKept ? ASTNodes.createMoveTarget(rewrite, modifier) : (Modifier) rewrite.createCopyTarget(modifier)); |
| } |
| } |
| |
| ASTNodes.replaceButKeepComment(rewrite, ASTNodes.getTypedAncestor(reassignmentAssignment, Statement.class), |
| newDeclareStatement, groupNewLocalVar); |
| |
| for (SimpleName occurrence : occurrences) { |
| if (occurrence != reassignment && occurrence.getParent() instanceof FieldAccess) { |
| ASTNodes.replaceButKeepComment(rewrite, occurrence.getParent(), occurrence, groupUsesOfTheVar); |
| } |
| } |
| } |
| } |
| } |