com.kalessil.phpStorm.phpInspectionsEA.inspectors.languageConstructions.ClassConstantCanBeUsedInspector.java Source code

Java tutorial

Introduction

Here is the source code for com.kalessil.phpStorm.phpInspectionsEA.inspectors.languageConstructions.ClassConstantCanBeUsedInspector.java

Source

package com.kalessil.phpStorm.phpInspectionsEA.inspectors.languageConstructions;

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.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.php.PhpIndex;
import com.jetbrains.php.config.PhpLanguageFeature;
import com.jetbrains.php.config.PhpLanguageLevel;
import com.jetbrains.php.config.PhpProjectConfigurationFacade;
import com.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment;
import com.jetbrains.php.lang.psi.PhpFile;
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 org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;

import javax.swing.*;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/*
 * 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 ClassConstantCanBeUsedInspector extends BasePhpInspection {
    // Inspection options.
    public boolean IMPORT_CLASSES_ON_QF = true;
    public boolean USE_RELATIVE_QF = true;
    public boolean LOOK_ROOT_NS_UP = false;

    private static final String messagePattern = "Perhaps this can be replaced with %c%::class.";
    private static final String messageUseStatic = "'static::class' can be used instead.";

    final static private Pattern classNameRegex;
    static {
        // Original regex: (\\(\\)?)?([a-zA-z0-9_]+\\(\\)?)?([a-zA-z0-9_]+)
        classNameRegex = Pattern.compile("(\\\\(\\\\)?)?([a-zA-z0-9_]+\\\\(\\\\)?)?([a-zA-z0-9_]+)");
    }

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

    @Override
    @NotNull
    public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
        return new BasePhpElementVisitor() {
            public void visitPhpFunctionCall(FunctionReference reference) {
                /* ensure selected language level supports the ::class feature*/
                final Project project = holder.getProject();
                final PhpLanguageLevel phpVersion = PhpProjectConfigurationFacade.getInstance(project)
                        .getLanguageLevel();
                if (!phpVersion.hasFeature(PhpLanguageFeature.CLASS_NAME_CONST)) {
                    return;
                }

                final String functionName = reference.getName();
                final PsiElement[] params = reference.getParameters();
                if (0 == params.length && null != functionName && functionName.equals("get_called_class")) {
                    final String replacement = "static::class";
                    holder.registerProblem(reference, messageUseStatic, ProblemHighlightType.WEAK_WARNING,
                            new UseStaticFix(replacement));
                }
            }

            public void visitPhpStringLiteralExpression(StringLiteralExpression expression) {
                /* ensure selected language level supports the ::class feature*/
                final Project project = holder.getProject();
                final PhpLanguageLevel phpVersion = PhpProjectConfigurationFacade.getInstance(project)
                        .getLanguageLevel();
                if (!phpVersion.hasFeature(PhpLanguageFeature.CLASS_NAME_CONST)) {
                    return;
                }

                /* Skip certain contexts processing and strings with inline injections */
                PsiElement parent = expression.getParent();
                if (parent instanceof BinaryExpression || parent instanceof SelfAssignmentExpression
                        || null != expression.getFirstPsiChild()) {
                    return;
                }

                /* Process if has no inline statements and at least 3 chars long (foo, bar and etc. are not a case) */
                final String contents = expression.getContents();
                if (contents.length() > 3) {
                    final Matcher regexMatcher = classNameRegex.matcher(contents);
                    if (!regexMatcher.matches()
                            || ExpressionSemanticUtil.getBlockScope(expression) instanceof PhpDocComment) {
                        return;
                    }
                    /* do not process lowercase-only strings */
                    if (-1 == contents.indexOf('\\') && contents.toLowerCase().equals(contents)) {
                        return;
                    }

                    String normalizedContents = contents.replaceAll("\\\\\\\\", "\\\\");

                    /* TODO: handle __NAMESPACE__.'\Class' */
                    final boolean isFull = normalizedContents.charAt(0) == '\\';
                    final Set<String> namesToLookup = new HashSet<>();
                    if (isFull) {
                        namesToLookup.add(normalizedContents);
                    } else {
                        if (LOOK_ROOT_NS_UP || normalizedContents.contains("\\")) {
                            normalizedContents = '\\' + normalizedContents;
                            namesToLookup.add(normalizedContents);
                        }
                    }

                    /* if we could find an appropriate candidate and resolved the class => report (case must match) */
                    if (1 == namesToLookup.size()) {
                        final String fqnToLookup = namesToLookup.iterator().next();
                        final PhpIndex index = PhpIndex.getInstance(project);

                        /* try searching interfaces and classes for the given FQN */
                        Collection<PhpClass> classes = index.getClassesByFQN(fqnToLookup);
                        if (0 == classes.size()) {
                            classes = index.getInterfacesByFQN(fqnToLookup);
                        }

                        /* check resolved items */
                        if (1 == classes.size() && classes.iterator().next().getFQN().equals(fqnToLookup)) {
                            final String message = messagePattern.replace("%c%", normalizedContents);
                            holder.registerProblem(expression, message, ProblemHighlightType.WEAK_WARNING,
                                    new TheLocalFix(normalizedContents, IMPORT_CLASSES_ON_QF, USE_RELATIVE_QF));
                        }
                    }
                    namesToLookup.clear();
                }
            }
        };
    }

    private class UseStaticFix extends UseSuggestedReplacementFixer {
        @NotNull
        @Override
        public String getName() {
            return "Use static::class instead.";
        }

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

    private static class TheLocalFix implements LocalQuickFix {
        final String fqn;
        final boolean importClasses;
        final boolean useRelativeQN;

        @NotNull
        @Override
        public String getName() {
            return "Use ::class instead.";
        }

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

        TheLocalFix(@NotNull String fqn, boolean importClasses, boolean useRelativeQN) {
            super();

            this.fqn = fqn;
            this.importClasses = importClasses;
            this.useRelativeQN = useRelativeQN;
        }

        @Override
        public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
            final PsiElement target = descriptor.getPsiElement();
            if (target instanceof StringLiteralExpression) {
                String classForReplacement = fqn;
                String className = classForReplacement.substring(1 + classForReplacement.lastIndexOf('\\'));

                synchronized (target.getContainingFile()) {
                    final PhpFile file = (PhpFile) target.getContainingFile();
                    boolean isImportedAlready = false;
                    boolean isImportNameCollision = false;
                    PsiElement importMarker = null;

                    /* check all use-statements and use imported name for QF */
                    for (PhpUseList use : PsiTreeUtil.findChildrenOfType(file, PhpUseList.class)) {
                        /* do not process `function() use () {}` constructs or class traits */
                        final PsiElement useParent = use.getParent();
                        if (useParent instanceof Function || useParent instanceof PhpClass) {
                            continue;
                        }

                        importMarker = use;
                        for (PsiElement used : use.getChildren()) {
                            if (!(used instanceof PhpUse)) {
                                continue;
                            }

                            final PhpUse useStatement = (PhpUse) used;
                            if (useStatement.getFQN().equals(fqn)) {
                                classForReplacement = useStatement.getName();
                                isImportedAlready = true;
                                break;
                            }

                            if (className.equals(useStatement.getName())) {
                                isImportNameCollision = true;
                            }
                        }
                    }

                    if (!isImportedAlready && !isImportNameCollision && importClasses) {
                        /* do not import classes from the root namespace */
                        boolean makesSense = StringUtils.countMatches(classForReplacement, "\\") > 1;
                        if (makesSense) {
                            boolean insertBefore = false;
                            boolean useRelativeQN = false;

                            /* identify marker/use relative QNs */
                            final PhpNamespace ns = PsiTreeUtil.findChildOfType(file, PhpNamespace.class);
                            if (null != ns) {
                                /* NS-ed file */
                                if (null == importMarker && ns.getLastChild() instanceof GroupStatement) {
                                    importMarker = ((GroupStatement) ns.getLastChild()).getFirstPsiChild();
                                    insertBefore = true;
                                }

                                if (this.useRelativeQN) {
                                    final String nsFQN = ns.getFQN() + '\\';
                                    if (classForReplacement.startsWith(nsFQN)) {
                                        classForReplacement = classForReplacement.replace(nsFQN, "");
                                        useRelativeQN = true;
                                    }
                                }
                            } else {
                                /* regular files, no NS */
                                if (file.getFirstPsiChild() instanceof GroupStatement) {
                                    importMarker = file.getFirstPsiChild().getFirstPsiChild();
                                    insertBefore = true;
                                }
                            }

                            /* inject new import after the marker, if relative QN are not possible */
                            if (null != importMarker && !useRelativeQN) {
                                final PhpUseList use = PhpPsiElementFactory.createUseStatement(project,
                                        classForReplacement, null);

                                if (insertBefore) {
                                    importMarker.getParent().addBefore(use, importMarker);
                                    final PsiElement space = PhpPsiElementFactory.createFromText(project,
                                            PsiWhiteSpace.class, "\n\n");
                                    if (null != space) {
                                        use.getParent().addAfter(space, use);
                                    }
                                } else {
                                    importMarker.getParent().addAfter(use, importMarker);
                                }

                                //noinspection ConstantConditions as we have hardcoded expression creation here
                                classForReplacement = use.getFirstPsiChild().getName();
                            }
                        }
                    }
                }

                PsiElement replacement = PhpPsiElementFactory.createFromText(project, ClassConstantReference.class,
                        classForReplacement + "::class");
                if (null != replacement) {
                    target.replace(replacement);
                }
            }
        }
    }

    public JComponent createOptionsPanel() {
        return OptionsComponent.create((component) -> {
            component.addCheckbox("Lookup root namespace classes", LOOK_ROOT_NS_UP,
                    (isSelected) -> LOOK_ROOT_NS_UP = isSelected);
            component.addCheckbox("Import classes automatically", IMPORT_CLASSES_ON_QF,
                    (isSelected) -> IMPORT_CLASSES_ON_QF = isSelected);
            component.addCheckbox("Use relative QN where possible", USE_RELATIVE_QF,
                    (isSelected) -> USE_RELATIVE_QF = isSelected);
        });
    }
}