com.kalessil.phpStorm.phpInspectionsEA.inspectors.apiUsage.strings.CascadeStringReplacementInspector.java Source code

Java tutorial

Introduction

Here is the source code for com.kalessil.phpStorm.phpInspectionsEA.inspectors.apiUsage.strings.CascadeStringReplacementInspector.java

Source

package com.kalessil.phpStorm.phpInspectionsEA.inspectors.apiUsage.strings;

import com.intellij.codeInspection.LocalQuickFix;
import com.intellij.codeInspection.ProblemDescriptor;
import com.intellij.codeInspection.ProblemHighlightType;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.LeafPsiElement;
import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment;
import com.jetbrains.php.lang.psi.PhpPsiElementFactory;
import com.jetbrains.php.lang.psi.elements.*;
import com.kalessil.phpStorm.phpInspectionsEA.fixers.UseSuggestedReplacementFixer;
import com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpElementVisitor;
import com.kalessil.phpStorm.phpInspectionsEA.openApi.BasePhpInspection;
import com.kalessil.phpStorm.phpInspectionsEA.options.OptionsComponent;
import com.kalessil.phpStorm.phpInspectionsEA.utils.ExpressionSemanticUtil;
import com.kalessil.phpStorm.phpInspectionsEA.utils.OpenapiEquivalenceUtil;
import com.kalessil.phpStorm.phpInspectionsEA.utils.OpenapiTypesUtil;
import org.apache.commons.lang.ArrayUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.util.*;

/*
 * This file is part of the Php Inspections (EA Extended) package.
 *
 * (c) Vladimir Reznichenko <kalessil@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

public class CascadeStringReplacementInspector extends BasePhpInspection {
    // Inspection options.
    public boolean USE_SHORT_ARRAYS_SYNTAX = false;

    private static final String messageNesting = "This str_replace(...) call can be merged with its parent.";
    private static final String messageCascading = "This str_replace(...) call can be merged with the previous.";
    private static final String messageReplacements = "Can be replaced with the string from the array.";

    @NotNull
    public String getShortName() {
        return "CascadeStringReplacementInspection";
    }

    @Override
    @NotNull
    public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
        return new BasePhpElementVisitor() {
            @Override
            public void visitPhpReturn(@NotNull PhpReturn returnStatement) {
                final FunctionReference functionCall = this.getFunctionReference(returnStatement);
                if (functionCall != null) {
                    this.analyze(functionCall, returnStatement);
                }
            }

            @Override
            public void visitPhpAssignmentExpression(@NotNull AssignmentExpression assignmentExpression) {
                final FunctionReference functionCall = this.getFunctionReference(assignmentExpression);
                if (functionCall != null) {
                    this.analyze(functionCall, assignmentExpression);
                }
            }

            private void analyze(@NotNull FunctionReference functionCall, @NotNull PsiElement expression) {
                final PsiElement[] arguments = functionCall.getParameters();
                if (arguments.length == 3) {
                    /* case: cascading replacements */
                    final AssignmentExpression previous = this.getPreviousAssignment(expression);
                    final FunctionReference previousCall = previous == null ? null
                            : this.getFunctionReference(previous);
                    if (previousCall != null) {
                        final PsiElement transitionVariable = previous.getVariable();
                        if (transitionVariable instanceof Variable && arguments[2] instanceof Variable) {
                            final Variable callSubject = (Variable) arguments[2];
                            final Variable previousVariable = (Variable) transitionVariable;
                            final PsiElement callResultStorage = expression instanceof AssignmentExpression
                                    ? ((AssignmentExpression) expression).getVariable()
                                    : callSubject;
                            if (callResultStorage != null
                                    && callSubject.getName().equals(previousVariable.getName())
                                    && OpenapiEquivalenceUtil.areEqual(transitionVariable, callResultStorage)) {
                                holder.registerProblem(functionCall, messageCascading,
                                        new MergeStringReplaceCallsFix(functionCall, previousCall,
                                                USE_SHORT_ARRAYS_SYNTAX));
                            }
                        }
                    }

                    /* case: nested replacements */
                    this.checkNestedCalls(arguments[2], functionCall);

                    /* case: search/replace simplification */
                    final PsiElement replace = arguments[1];
                    if (replace instanceof ArrayCreationExpression) {
                        this.checkForSimplification((ArrayCreationExpression) replace);
                    } else if (replace instanceof StringLiteralExpression) {
                        final PsiElement search = arguments[0];
                        if (search instanceof ArrayCreationExpression) {
                            this.checkForSimplification((ArrayCreationExpression) search);
                        }
                    }
                }
            }

            private void checkForSimplification(@NotNull ArrayCreationExpression candidate) {
                final Set<String> replacements = new HashSet<>();
                for (final PsiElement oneReplacement : candidate.getChildren()) {
                    if (oneReplacement instanceof PhpPsiElement) {
                        final PhpPsiElement item = ((PhpPsiElement) oneReplacement).getFirstPsiChild();
                        if (!(item instanceof StringLiteralExpression)) {
                            replacements.clear();
                            return;
                        }
                        replacements.add(item.getText());
                    }
                }
                if (replacements.size() == 1) {
                    holder.registerProblem(candidate, messageReplacements, ProblemHighlightType.WEAK_WARNING,
                            new SimplifyReplacementFix(replacements.iterator().next()));
                }
                replacements.clear();
            }

            private void checkNestedCalls(@NotNull PsiElement callCandidate,
                    @NotNull FunctionReference parentCall) {
                if (OpenapiTypesUtil.isFunctionReference(callCandidate)) {
                    final FunctionReference call = (FunctionReference) callCandidate;
                    final String functionName = call.getName();
                    if (functionName != null && functionName.equals("str_replace")) {
                        holder.registerProblem(callCandidate, messageNesting,
                                new MergeStringReplaceCallsFix(parentCall, call, USE_SHORT_ARRAYS_SYNTAX));
                    }
                }
            }

            @Nullable
            private FunctionReference getFunctionReference(@NotNull AssignmentExpression assignment) {
                FunctionReference result = null;
                final PsiElement value = ExpressionSemanticUtil
                        .getExpressionTroughParenthesis(assignment.getValue());
                if (OpenapiTypesUtil.isFunctionReference(value)) {
                    final String functionName = ((FunctionReference) value).getName();
                    if (functionName != null && functionName.equals("str_replace")) {
                        result = (FunctionReference) value;
                    }
                }
                return result;
            }

            @Nullable
            private FunctionReference getFunctionReference(@NotNull PhpReturn phpReturn) {
                FunctionReference result = null;
                final PsiElement value = ExpressionSemanticUtil
                        .getExpressionTroughParenthesis(ExpressionSemanticUtil.getReturnValue(phpReturn));
                if (OpenapiTypesUtil.isFunctionReference(value)) {
                    final String functionName = ((FunctionReference) value).getName();
                    if (functionName != null && functionName.equals("str_replace")) {
                        result = (FunctionReference) value;
                    }
                }
                return result;
            }

            @Nullable
            private AssignmentExpression getPreviousAssignment(@NotNull PsiElement returnOrAssignment) {
                /* get previous non-comment, non-php-doc expression */
                PsiElement previous = null;
                if (returnOrAssignment instanceof PhpReturn) {
                    previous = ((PhpReturn) returnOrAssignment).getPrevPsiSibling();
                } else if (returnOrAssignment instanceof AssignmentExpression) {
                    previous = returnOrAssignment.getParent().getPrevSibling();
                }
                while (previous != null && !(previous instanceof PhpPsiElement)) {
                    previous = previous.getPrevSibling();
                }
                while (previous instanceof PhpDocComment) {
                    previous = ((PhpDocComment) previous).getPrevPsiSibling();
                }
                /* grab the target assignment */
                final AssignmentExpression result;
                if (previous != null && previous.getFirstChild() instanceof AssignmentExpression) {
                    result = (AssignmentExpression) previous.getFirstChild();
                } else {
                    result = null;
                }
                return result;
            }
        };
    }

    public JComponent createOptionsPanel() {
        return OptionsComponent.create((component) -> component.addCheckbox("Use short arrays syntax",
                USE_SHORT_ARRAYS_SYNTAX, (isSelected) -> USE_SHORT_ARRAYS_SYNTAX = isSelected));
    }

    private static final class SimplifyReplacementFix extends UseSuggestedReplacementFixer {
        private static final String title = "Simplify this argument";

        @NotNull
        @Override
        public String getName() {
            return title;
        }

        SimplifyReplacementFix(@NotNull String expression) {
            super(expression);
        }
    }

    private static final class MergeStringReplaceCallsFix implements LocalQuickFix {
        private static final String title = "Merge str_replace(...) calls";

        final private SmartPsiElementPointer<FunctionReference> patch;
        final private SmartPsiElementPointer<FunctionReference> eliminate;
        final private boolean useShortSyntax;

        MergeStringReplaceCallsFix(@NotNull FunctionReference patch, @NotNull FunctionReference eliminate,
                boolean useShortSyntax) {
            super();
            final SmartPointerManager factory = SmartPointerManager.getInstance(patch.getProject());

            this.patch = factory.createSmartPsiElementPointer(patch);
            this.eliminate = factory.createSmartPsiElementPointer(eliminate);
            this.useShortSyntax = useShortSyntax;
        }

        @NotNull
        @Override
        public String getName() {
            return title;
        }

        @NotNull
        @Override
        public String getFamilyName() {
            return title;
        }

        @Override
        public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
            final FunctionReference patch = this.patch.getElement();
            final FunctionReference eliminate = this.eliminate.getElement();
            if (patch != null && eliminate != null && !project.isDisposed()) {
                synchronized (eliminate.getContainingFile()) {
                    this.mergeReplaces(patch, eliminate, this.useShortSyntax);
                    this.mergeArguments(patch.getParameters()[0], eliminate.getParameters()[0],
                            this.useShortSyntax);
                    this.mergeSources(patch, eliminate);
                }
            }
        }

        private void mergeArguments(@NotNull PsiElement to, @NotNull PsiElement from, boolean useShortSyntax) {
            final Project project = to.getProject();
            if (to instanceof ArrayCreationExpression) {
                final PsiElement comma = PhpPsiElementFactory.createFromText(project, LeafPsiElement.class, ",");
                final PsiElement firstValue = ((ArrayCreationExpression) to).getFirstPsiChild();
                final PsiElement marker = firstValue == null ? null : firstValue.getPrevSibling();
                if (comma != null && marker != null) {
                    if (from instanceof ArrayCreationExpression) {
                        final PsiElement[] values = from.getChildren();
                        ArrayUtils.reverse(values);
                        Arrays.stream(values).forEach(value -> {
                            to.addAfter(comma, marker);
                            to.addAfter(value.copy(), marker);
                        });
                    } else {
                        to.addAfter(comma, marker);
                        to.addAfter(from.copy(), marker);
                    }
                }
            } else {
                if (from instanceof ArrayCreationExpression) {
                    final PsiElement comma = PhpPsiElementFactory.createFromText(project, LeafPsiElement.class,
                            ",");
                    final String pattern = String.format(useShortSyntax ? "[%s]" : "array(%s)", to.getText());
                    final ArrayCreationExpression replacement = PhpPsiElementFactory.createPhpPsiFromText(project,
                            ArrayCreationExpression.class, pattern);
                    final PsiElement firstValue = replacement.getFirstPsiChild();
                    final PsiElement marker = firstValue == null ? null : firstValue.getPrevSibling();
                    if (comma != null && marker != null) {
                        final PsiElement[] values = from.getChildren();
                        ArrayUtils.reverse(values);
                        Arrays.stream(values).forEach(value -> {
                            replacement.addAfter(comma, marker);
                            replacement.addAfter(value.copy(), marker);
                        });
                        to.replace(replacement);
                    }
                } else {
                    final String pattern = String.format(useShortSyntax ? "[%s, %s]" : "array(%s, %s)",
                            from.getText(), to.getText());
                    to.replace(PhpPsiElementFactory.createPhpPsiFromText(project, ArrayCreationExpression.class,
                            pattern));
                }
            }
        }

        @NotNull
        private PsiElement unbox(@NotNull PsiElement what) {
            if (what instanceof ArrayCreationExpression) {
                final PsiElement[] elements = what.getChildren();
                if (elements.length == 1 && !(elements[0] instanceof ArrayHashElement)) {
                    final PsiElement value = elements[0].getFirstChild();
                    if (value instanceof StringLiteralExpression) {
                        what = value;
                    }
                }
            }
            return what;
        }

        private void mergeReplaces(@NotNull FunctionReference to, @NotNull FunctionReference from,
                boolean useShortSyntax) {
            /* normalization here */
            final PsiElement fromNormalized = this.unbox(from.getParameters()[1]);
            final PsiElement toRaw = to.getParameters()[1];
            final PsiElement toNormalized = this.unbox(toRaw);

            /* a little bit of intelligence */
            boolean needsFurtherFixing = true;
            if (toNormalized instanceof StringLiteralExpression) {
                if (fromNormalized instanceof StringLiteralExpression
                        && fromNormalized.getText().equals(toNormalized.getText())) {
                    toRaw.replace(toNormalized);
                    needsFurtherFixing = false;
                }
            }

            if (needsFurtherFixing) {
                /* in order to perform the proper merging we'll need to expand short-hand replacement definitions */
                this.expandReplacement(to, useShortSyntax);
                this.expandReplacement(from, useShortSyntax);
                this.mergeArguments(to.getParameters()[1], from.getParameters()[1], useShortSyntax);
            }
        }

        private void expandReplacement(@NotNull FunctionReference call, boolean useShortSyntax) {
            final PsiElement[] arguments = call.getParameters();
            final PsiElement search = arguments[0];
            final PsiElement replace = arguments[1];
            if (replace instanceof StringLiteralExpression && search instanceof ArrayCreationExpression) {
                final int searchesCount = search.getChildren().length;
                if (searchesCount > 1) {
                    final List<String> replaces = Collections.nCopies(searchesCount, replace.getText());
                    replace.replace(PhpPsiElementFactory.createPhpPsiFromText(call.getProject(),
                            ArrayCreationExpression.class,
                            String.format(useShortSyntax ? "[%s]" : "array(%s)", String.join(", ", replaces))));
                }
            }
        }

        private void mergeSources(@NotNull FunctionReference patch, @NotNull FunctionReference eliminate) {
            final PsiElement eliminateParent = eliminate.getParent().getParent();
            patch.getParameters()[2].replace(eliminate.getParameters()[2]);
            if (OpenapiTypesUtil.isStatementImpl(eliminateParent)) {
                final PsiElement trailingSpaceCandidate = eliminateParent.getNextSibling();
                if (trailingSpaceCandidate instanceof PsiWhiteSpace) {
                    trailingSpaceCandidate.delete();
                }
                eliminateParent.delete();
            }
        }
    }
}