org.autorefactor.refactoring.rules.EntrySetRatherThanKeySetAndValueSearchRefactoring.java Source code

Java tutorial

Introduction

Here is the source code for org.autorefactor.refactoring.rules.EntrySetRatherThanKeySetAndValueSearchRefactoring.java

Source

/*
 * AutoRefactor - Eclipse plugin to automatically refactor Java code bases.
 *
 * Copyright (C) 2016 Zsombor Gegesy - initial API and implementation
 * Copyright (C) 2016 Jean-Nol Rouvignac - code cleanups
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program under LICENSE-GNUGPL.  If not, see
 * <http://www.gnu.org/licenses/>.
 *
 *
 * 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 under LICENSE-ECLIPSE, and is
 * available at http://www.eclipse.org/legal/epl-v10.html
 */
package org.autorefactor.refactoring.rules;

import static org.autorefactor.refactoring.ASTHelper.DO_NOT_VISIT_SUBTREE;
import static org.autorefactor.refactoring.ASTHelper.VISIT_SUBTREE;
import static org.autorefactor.refactoring.ASTHelper.areBindingsEqual;
import static org.autorefactor.refactoring.ASTHelper.arg0;
import static org.autorefactor.refactoring.ASTHelper.asList;
import static org.autorefactor.refactoring.ASTHelper.getFirstAncestorOrNull;
import static org.autorefactor.refactoring.ASTHelper.isMethod;
import static org.autorefactor.refactoring.ASTHelper.isSameVariable;
import static org.eclipse.jdt.core.dom.ASTNode.ANNOTATION_TYPE_DECLARATION;
import static org.eclipse.jdt.core.dom.ASTNode.ANONYMOUS_CLASS_DECLARATION;
import static org.eclipse.jdt.core.dom.ASTNode.ENUM_DECLARATION;
import static org.eclipse.jdt.core.dom.ASTNode.METHOD_INVOCATION;
import static org.eclipse.jdt.core.dom.ASTNode.TYPE_DECLARATION;
import static org.eclipse.jdt.core.dom.EnhancedForStatement.EXPRESSION_PROPERTY;
import static org.eclipse.jdt.core.dom.EnhancedForStatement.PARAMETER_PROPERTY;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.autorefactor.refactoring.ASTBuilder;
import org.autorefactor.refactoring.CollectorVisitor;
import org.autorefactor.refactoring.Refactorings;
import org.autorefactor.refactoring.TypeNameDecider;
import org.autorefactor.refactoring.Variable;
import org.autorefactor.util.IllegalStateException;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.EnhancedForStatement;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.Initializer;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.MethodInvocation;
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.Type;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;

/** See {@link #getDescription()} method. */
public class EntrySetRatherThanKeySetAndValueSearchRefactoring extends AbstractRefactoringRule {
    /**
     * Get the name.
     *
     * @return the name.
     */
    public String getName() {
        return "Map.entrySet() rather than Map.keySet() and value search";
    }

    /**
     * Get the description.
     *
     * @return the description.
     */
    public String getDescription() {
        return "" + "Convert for loops iterating on Map.keySet() to iterate on Map.entrySet() when possible.";
    }

    /**
     * Get the reason.
     *
     * @return the reason.
     */
    public String getReason() {
        return "It reduces the coding, reading, debugging and testing cost."
                + " It also improves the time and the space performance.";
    }

    /**
     * This class helps decide which name to give to a new variable.
     * <p>
     * When creating a new variable, its name may shadow another variable or field used in the local
     * scope.
     * <p>
     * Does JDT provide a public API for naming local variables? I could not find any.
     */
    private static final class VariableNameDecider {
        private final ASTNode scope;
        private final int insertionPoint;
        private final ASTNode namingScope;

        private VariableNameDecider(ASTNode scope, int insertionPoint) {
            this.scope = scope;
            this.insertionPoint = insertionPoint;
            this.namingScope = getNamingScope(scope);
        }

        private ASTNode getNamingScope(ASTNode scope) {
            Class<?>[] ancestorClasses = new Class<?>[] { MethodDeclaration.class, Initializer.class };
            ASTNode ancestor = getFirstAncestorOrNull(scope, ancestorClasses);
            if (ancestor == null) {
                throw new IllegalStateException(scope, "Expected to find an ancestor among the types "
                        + Arrays.toString(ancestorClasses) + " but could not find any");
            }
            return ancestor;
        }

        /**
         * Returns a name suggestion suitable for use when inserting a new variable declaration. This
         * name:
         * <ul>
         * <li>will not shadow any variable name in use after the insertion point</li>
         * <li>and will not conflict with local variables declared before the insertion point.</li>
         * </ul>
         *
         * @param candidateNames
         *          the suggestion will be one of the candidate names, maybe suffixed by a number
         * @return the suggestion for a variable name
         */
        public String suggest(String... candidateNames) {
            final Set<String> declaredLocalVarNames = new HashSet<String>(collectDeclaredLocalVariableNames());
            final Set<String> varNamesUsedAfter = new HashSet<String>(collectVariableNamesUsedAfter());
            // Can we use one of the candidate names?
            for (String candidate : candidateNames) {
                if (isSuitable(candidate, declaredLocalVarNames, varNamesUsedAfter)) {
                    return candidate;
                }
            }

            // Iterate on the first candidate name and suffix it with an integer
            int i = 1;
            do {
                final String candidate = candidateNames[0] + i;
                if (isSuitable(candidate, declaredLocalVarNames, varNamesUsedAfter)) {
                    return candidate;
                }
                i++;
            } while (true);
        }

        private boolean isSuitable(String candidateName, Set<String> declaredLocalVarNames,
                Set<String> varNamesUsedAfter) {
            return // no variable declaration conflict
            !declaredLocalVarNames.contains(candidateName)
                    // new variable does not shadow use of other variables/fields with the same name
                    && !varNamesUsedAfter.contains(candidateName);
        }

        private Collection<String> collectDeclaredLocalVariableNames() {
            return new CollectorVisitor<String>() {
                @Override
                public boolean preVisit2(ASTNode node) {
                    return !isTypeDeclaration(node);
                }

                @Override
                public boolean visit(SimpleName node) {
                    final IBinding binding = node.resolveBinding();
                    if (binding.getKind() == IBinding.VARIABLE) {
                        addResult(((IVariableBinding) binding).getName());
                    }
                    return VISIT_SUBTREE;
                }
            }.collect(namingScope);
        }

        private List<String> collectVariableNamesUsedAfter() {
            return new CollectorVisitor<String>() {
                @Override
                public boolean preVisit2(ASTNode node) {
                    return node.getStartPosition() > insertionPoint && !isTypeDeclaration(node);
                }

                @Override
                public boolean visit(SimpleName node) {
                    final IBinding binding = node.resolveBinding();
                    if (binding.getKind() == IBinding.VARIABLE) {
                        addResult(((IVariableBinding) binding).getName());
                    }
                    return VISIT_SUBTREE;
                }
            }.collect(scope);
        }

        private boolean isTypeDeclaration(ASTNode node) {
            switch (node.getNodeType()) {
            case ANNOTATION_TYPE_DECLARATION:
            case ANONYMOUS_CLASS_DECLARATION:
            case ENUM_DECLARATION:
            case TYPE_DECLARATION:
                return true;

            default:
                return false;
            }
        }
    }

    @Override
    public boolean visit(EnhancedForStatement enhancedFor) {
        final Expression foreachExpr = enhancedFor.getExpression();
        if (isKeySetMethod(foreachExpr)) {
            // From 'for (K key : map.keySet()) { }'
            // -> mapExpression become 'map', parameter become 'K key'
            final Expression mapExpression = ((MethodInvocation) foreachExpr).getExpression();
            if (mapExpression == null) {
                // not implemented
                return VISIT_SUBTREE;
            }
            final SingleVariableDeclaration parameter = enhancedFor.getParameter();
            final List<MethodInvocation> getValueMis = collectMapGetValueCalls(mapExpression, parameter,
                    enhancedFor.getBody());
            if (!getValueMis.isEmpty() && haveSameTypeBindings(getValueMis)) {
                replaceEntryIterationByKeyIteration(enhancedFor, mapExpression, parameter, getValueMis);
                return DO_NOT_VISIT_SUBTREE;
            }
        }

        return VISIT_SUBTREE;
    }

    private void replaceEntryIterationByKeyIteration(EnhancedForStatement enhancedFor,
            final Expression mapExpression, final SingleVariableDeclaration parameter,
            final List<MethodInvocation> getValueMis) {
        final ASTBuilder b = ctx.getASTBuilder();
        final Refactorings r = ctx.getRefactorings();

        final VariableDefinitionsUsesVisitor keyUseVisitor = new VariableDefinitionsUsesVisitor(parameter);
        enhancedFor.getBody().accept(keyUseVisitor);
        int keyUses = keyUseVisitor.getUses().size();

        final int insertionPoint = asList(enhancedFor.getBody()).get(0).getStartPosition() - 1;
        final Variable entryVar = new Variable(
                new VariableNameDecider(enhancedFor.getBody(), insertionPoint).suggest("entry", "mapEntry"), b);
        final TypeNameDecider typeNameDecider = new TypeNameDecider(parameter);

        final MethodInvocation getValueMi0 = getValueMis.get(0);
        final ITypeBinding typeBinding = getValueMi0.getExpression().resolveTypeBinding();
        if (typeBinding != null && typeBinding.isRawType()) {
            // for (Object key : map.keySet()) => for (Object key : map.entrySet())
            r.set(enhancedFor, EXPRESSION_PROPERTY, b.invoke(b.move(mapExpression), "entrySet"));
            final Type objectType = b.type(typeNameDecider.useSimplestPossibleName("java.lang.Object"));
            final Variable objectVar = new Variable(
                    new VariableNameDecider(enhancedFor.getBody(), insertionPoint).suggest("obj"), b);
            r.set(enhancedFor, PARAMETER_PROPERTY, b.declareSingleVariable(objectVar.varNameRaw(), objectType));

            // for (Map.Entry<K, V> mapEntry : map.entrySet()) {
            //     Map.Entry mapEntry = (Map.Entry) obj; // <--- add this statement
            //     Object key = mapEntry.getKey(); // <--- add this statement

            final Type mapKeyType = b.copy(parameter.getType());
            final VariableDeclarationStatement newKeyDecl = b.declareStmt(mapKeyType, b.move(parameter.getName()),
                    b.invoke(entryVar.varName(), "getKey"));

            r.insertFirst(enhancedFor.getBody(), Block.STATEMENTS_PROPERTY, newKeyDecl);

            if (keyUses > getValueMis.size()) {
                String mapEntryTypeName = typeNameDecider.useSimplestPossibleName("java.util.Map.Entry");

                final VariableDeclarationStatement newEntryDecl = b.declareStmt(b.type(mapEntryTypeName),
                        entryVar.varName(), b.cast(b.type(mapEntryTypeName), objectVar.varName()));
                r.insertFirst(enhancedFor.getBody(), Block.STATEMENTS_PROPERTY, newEntryDecl);
            }
        } else {
            // for (K key : map.keySet()) => for (K key : map.entrySet())
            r.set(enhancedFor, EXPRESSION_PROPERTY, b.invoke(b.move(mapExpression), "entrySet"));
            // for (K key : map.entrySet()) => for (Map.Entry<K, V> mapEntry : map.entrySet())
            final Type mapEntryType = createMapEntryType(parameter, getValueMi0, typeNameDecider);
            r.set(enhancedFor, PARAMETER_PROPERTY, b.declareSingleVariable(entryVar.varNameRaw(), mapEntryType));

            if (keyUses > getValueMis.size()) {
                // for (Map.Entry<K, V> mapEntry : map.entrySet()) {
                //     K key = mapEntry.getKey(); // <--- add this statement
                final Type mapKeyType = b.copy(parameter.getType());

                final VariableDeclarationStatement newKeyDeclaration = b.declareStmt(mapKeyType,
                        b.move(parameter.getName()), b.invoke(entryVar.varName(), "getKey"));
                r.insertFirst(enhancedFor.getBody(), Block.STATEMENTS_PROPERTY, newKeyDeclaration);
            }
        }

        // Replace all occurrences of map.get(key) => mapEntry.getValue()
        for (MethodInvocation getValueMi : getValueMis) {
            r.replace(getValueMi, b.invoke(entryVar.varName(), "getValue"));
        }
    }

    /**
     * If possible, use the type declaration, so we can return the type as it was declared.
     * Otherwise, let's use the type binding and output verbose fully qualified types.
     */
    private Type createMapEntryType(SingleVariableDeclaration parameter, MethodInvocation getValueMi,
            TypeNameDecider typeNameDecider) {
        final String mapEntryType = typeNameDecider.useSimplestPossibleName("java.util.Map.Entry");

        final ASTBuilder b = ctx.getASTBuilder();
        final Type paramType = parameter.getType();
        final Type mapKeyType;
        if (paramType.isPrimitiveType()) {
            // Use the type binding (not as precise as what is in the code)
            final ITypeBinding mapTypeBinding = getValueMi.getExpression().resolveTypeBinding();
            final ITypeBinding keyTypeBinding = mapTypeBinding.getTypeArguments()[0];
            mapKeyType = b.toType(keyTypeBinding, typeNameDecider);
        } else {
            // Use the type as defined in the code
            mapKeyType = b.move(paramType);
        }
        final Type mapValueType = b.copyType(getValueMi, typeNameDecider);
        return b.genericType(mapEntryType, mapKeyType, mapValueType);
    }

    private boolean isKeySetMethod(Expression expr) {
        return expr instanceof MethodInvocation && isMethod((MethodInvocation) expr, "java.util.Map", "keySet");
    }

    private List<MethodInvocation> collectMapGetValueCalls(Expression mapExpression,
            SingleVariableDeclaration parameter, Statement body) {
        return new CollectMapGetCalls(mapExpression, parameter).collect(body);
    }

    /** Sanity check. */
    private boolean haveSameTypeBindings(Collection<? extends Expression> exprs) {
        Iterator<? extends Expression> it = exprs.iterator();
        if (!it.hasNext()) {
            // Not really expected
            return false;
        }
        final ITypeBinding type0 = it.next().resolveTypeBinding();
        if (type0 == null) {
            return false;
        }
        while (it.hasNext()) {
            final ITypeBinding typeN = it.next().resolveTypeBinding();
            if (!areSameTypeBindings(type0, typeN)) {
                return false;
            }
        }
        return true;
    }

    private boolean areSameTypeBindings(final ITypeBinding type1, final ITypeBinding type2) {
        if (type1 == null || type2 == null) {
            return true;
        }
        return type1.isParameterizedType() == type2.isParameterizedType()
                && areSameParameterizedTypeBindings(type1, type2);
    }

    /** Special handling because of captures. */
    private boolean areSameParameterizedTypeBindings(final ITypeBinding type1, final ITypeBinding type2) {
        return type1.getErasure().equals(type2.getErasure())
                && areSameTypeBindings(type1.getTypeArguments(), type2.getTypeArguments());
    }

    private boolean areSameTypeBindings(ITypeBinding[] types1, ITypeBinding[] types2) {
        if (types1.length != types2.length) {
            return false;
        }
        for (int i = 0; i < types1.length; i++) {
            if (!areSameTypeBindings(types1[i], types2[i])) {
                return false;
            }
        }
        return true;
    }

    private boolean areSameTypeBindingsByAliasingTypeCaptures(final ITypeBinding type1, final ITypeBinding type2) {
        if (type1.isCapture() ^ type2.isCapture()) {
            return false;
        } else if (type1.isCapture()) {
            return areSameTypeBindings(type1.getWildcard(), type2.getWildcard());
        } else {
            return type1.equals(type2);
        }
    }

    /**
     * Class to find {@code map.get(loopVariable)} constructs in the AST tree,
     * and collect the type of the value, which is unknown until one is located.
     */
    class CollectMapGetCalls extends CollectorVisitor<MethodInvocation> {
        private final Expression mapExpression;
        private final SingleVariableDeclaration forEachParameter;

        public CollectMapGetCalls(Expression mapExpression, SingleVariableDeclaration forEachParameter) {
            this.mapExpression = mapExpression;
            this.forEachParameter = forEachParameter;
        }

        @Override
        public boolean visit(MethodInvocation node) {
            if (isSameReference(node.getExpression(), mapExpression)
                    && isMethod(node, "java.util.Map", "get", "java.lang.Object")
                    && isSameVariable(arg0(node), forEachParameter.getName())) {
                addResult(node);
            }
            return VISIT_SUBTREE;
        }

        private boolean isSameReference(Expression expr1, Expression expr2) {
            if (expr1 == null || expr2 == null) {
                return false;
            } else if (expr1.getNodeType() != METHOD_INVOCATION || expr2.getNodeType() != METHOD_INVOCATION) {
                return isSameVariable(expr1, expr2);
            } else {
                final MethodInvocation mi1 = (MethodInvocation) expr1;
                final MethodInvocation mi2 = (MethodInvocation) expr2;
                return areBindingsEqual(mi1.resolveTypeBinding(), mi2.resolveTypeBinding())
                        && isSameReference(mi1.getExpression(), mi2.getExpression());
            }
        }
    }
}