com.kalessil.phpStorm.yii2inspections.inspectors.MissingPropertyAnnotationsInspector.java Source code

Java tutorial

Introduction

Here is the source code for com.kalessil.phpStorm.yii2inspections.inspectors.MissingPropertyAnnotationsInspector.java

Source

package com.kalessil.phpStorm.yii2inspections.inspectors;

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.jetbrains.php.lang.documentation.phpdoc.psi.PhpDocComment;
import com.jetbrains.php.lang.inspections.PhpInspection;
import com.jetbrains.php.lang.psi.PhpPsiElementFactory;
import com.jetbrains.php.lang.psi.elements.*;
import com.jetbrains.php.lang.psi.visitors.PhpElementVisitor;
import com.kalessil.phpStorm.yii2inspections.inspectors.utils.InheritanceChainExtractUtil;
import com.kalessil.phpStorm.yii2inspections.inspectors.utils.NamedElementUtil;
import net.miginfocom.swing.MigLayout;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.NotNull;

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

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

final public class MissingPropertyAnnotationsInspector extends PhpInspection {
    // configuration flags automatically saved by IDE
    @SuppressWarnings("WeakerAccess")
    public boolean REQUIRE_BOTH_GETTER_SETTER = false;

    private static final String messagePattern = "'%p%': properties needs to be annotated";

    private static final Set<String> baseObjectClasses = new HashSet<>();
    static {
        baseObjectClasses.add("\\yii\\base\\Object");
        baseObjectClasses.add("\\yii\\base\\BaseObject");
    }

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

    @Override
    @NotNull
    public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) {
        return new PhpElementVisitor() {
            @Override
            public void visitPhpClass(PhpClass clazz) {
                /* check only regular named classes */
                final PsiElement nameNode = NamedElementUtil.getNameIdentifier(clazz);
                if (null == nameNode) {
                    return;
                }

                /* check if the class inherited from yii\base\Object */
                boolean supportsPropertyFeature = false;
                final Set<PhpClass> parents = InheritanceChainExtractUtil.collect(clazz);
                if (!parents.isEmpty()) {
                    for (final PhpClass parent : parents) {
                        if (baseObjectClasses.contains(parent.getFQN())) {
                            supportsPropertyFeature = true;
                            break;
                        }
                    }

                    parents.clear();
                }
                if (!supportsPropertyFeature) {
                    return;
                }

                /* iterate get methods, find matching set methods */
                final Map<String, String> props = this.findPropertyCandidates(clazz);
                if (props.size() > 0) {
                    List<String> names = new ArrayList<>(props.keySet());
                    Collections.sort(names);
                    final String message = messagePattern.replace("%p%", String.join("', '", names));
                    holder.registerProblem(nameNode, message, ProblemHighlightType.WEAK_WARNING,
                            new TheLocalFix(props));
                }
            }

            @NotNull
            private Map<String, String> findPropertyCandidates(@NotNull PhpClass clazz) {
                final Map<String, String> properties = new HashMap<>();

                /* extract methods and operate on name-methods relations */
                final Method[] methods = clazz.getOwnMethods();
                if (null == methods || 0 == methods.length) {
                    return properties;
                }
                final Map<String, Method> mappedMethods = new HashMap<>();
                for (Method method : methods) {
                    mappedMethods.put(method.getName(), method);
                }

                /* process extracted methods*/
                for (String candidate : mappedMethods.keySet()) {
                    Method getterMethod = null;
                    Method setterMethod = null;

                    /* extract methods: get (looks up and extracts set), set (looks up get and skipped if found) */
                    if (candidate.startsWith("get")) {
                        getterMethod = mappedMethods.get(candidate);
                        if (getterMethod.isStatic() || 0 != getterMethod.getParameters().length) {
                            getterMethod = null;
                        }

                        final String complimentarySetter = candidate.replaceAll("^get", "set");
                        if (mappedMethods.containsKey(complimentarySetter)) {
                            setterMethod = mappedMethods.get(complimentarySetter);
                            if (setterMethod.isStatic() || 0 == setterMethod.getParameters().length) {
                                setterMethod = null;
                            }

                        }
                    }
                    if (candidate.startsWith("set")) {
                        setterMethod = mappedMethods.get(candidate);
                        if (setterMethod.isStatic() || setterMethod.getParameters().length != 1) {
                            setterMethod = null;
                        }

                        final String complimentaryGetter = candidate.replaceAll("^set", "get");
                        if (mappedMethods.containsKey(complimentaryGetter)) {
                            continue;
                        }
                    }

                    /* ensure that strategies are reachable */
                    if ((null == getterMethod && null == setterMethod)
                            || (REQUIRE_BOTH_GETTER_SETTER && (null == getterMethod || null == setterMethod))) {
                        continue;
                    }

                    /* store property and it's types */
                    final Set<String> propertyTypesFqns = new HashSet<>();

                    if (null != getterMethod) {
                        propertyTypesFqns.addAll(getterMethod.getType().filterUnknown().getTypes());
                    }
                    if (null != setterMethod) {
                        final Parameter[] setterParams = setterMethod.getParameters();
                        if (setterParams.length > 0) {
                            propertyTypesFqns.addAll(setterParams[0].getType().filterUnknown().getTypes());
                        }
                    }

                    /* drop preceding \ in core types */
                    final Set<String> propertyTypes = new HashSet<>();
                    for (String type : propertyTypesFqns) {
                        if (type.length() > 0) {
                            if ('\\' == type.charAt(0) && 1 == StringUtils.countMatches(type, "\\")) {
                                type = type.replace("\\", "");
                            }
                            propertyTypes.add(type);
                        }
                    }
                    propertyTypesFqns.clear();

                    final String typesAsString = propertyTypes.isEmpty() ? "mixed"
                            : String.join("|", propertyTypes);
                    properties.put(StringUtils.uncapitalize(candidate.replaceAll("^(get|set)", "")), typesAsString);
                }

                /* exclude annotated properties: lazy bulk operation */
                if (properties.size() > 0) {
                    final Collection<Field> fields = clazz.getFields();
                    for (Field candidate : fields) {
                        /* do not process constants and static fields */
                        if (candidate.isConstant() || candidate.getModifier().isStatic()) {
                            continue;
                        }

                        properties.remove(candidate.getName());
                    }
                    fields.clear();
                }

                return properties;
            }
        };
    }

    private static class TheLocalFix implements LocalQuickFix {
        final private Map<String, String> properties;

        TheLocalFix(@NotNull Map<String, String> properties) {
            super();
            this.properties = properties;
        }

        @NotNull
        @Override
        public String getName() {
            return "Annotate properties";
        }

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

        @Override
        public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
            final PsiElement expression = descriptor.getPsiElement();
            final PsiElement clazzCandidate = null == expression ? null : expression.getParent();
            if (clazzCandidate instanceof PhpClass) {
                final PhpClass clazz = (PhpClass) clazzCandidate;
                PhpPsiElement previous = clazz.getPrevPsiSibling();

                /* inject new DocBlock before the class if needed */
                if (!(previous instanceof PhpDocComment)) {
                    /* injection marker needed due to psi-tree structure for NS-ed and not NS-ed classes */
                    final PsiElement injectionMarker;
                    if (null == previous && clazz.getParent() instanceof GroupStatement) {
                        previous = ((GroupStatement) clazz.getParent()).getPrevPsiSibling();
                        injectionMarker = previous instanceof PhpDocComment ? null : clazz.getParent();
                    } else {
                        injectionMarker = clazz;
                    }

                    PsiElement block = PhpPsiElementFactory.createFromText(project, PhpDocComment.class,
                            "/**\n */\n");
                    if (null != injectionMarker && null != block) {
                        injectionMarker.getParent().addBefore(block, injectionMarker);
                        previous = clazz.getPrevPsiSibling();
                    }
                }

                /* perform injection into the DocBlock */
                if (previous instanceof PhpDocComment) {
                    /* reassemble for processing */
                    final LinkedList<String> lines = new LinkedList<>(
                            Arrays.asList(previous.getText().split("\\n")));

                    /* check if we have already properties */
                    int injectionIndex = 0;
                    for (int i = lines.size() - 1; i > 0; --i) {
                        if (lines.get(i).contains("@property")) {
                            injectionIndex = i;
                            break;
                        }
                    }

                    /* inject properties definition */
                    final String pattern = lines.peekLast().replaceAll("[\\s/]+$", " ");
                    if (0 == injectionIndex) {
                        lines.add(lines.size() - 1, pattern);
                    }
                    for (String propertyName : this.properties.keySet()) {
                        final String types = this.properties.get(propertyName);
                        final String newLine = pattern + "@property " + types + " $" + propertyName;

                        lines.add((injectionIndex > 0 ? injectionIndex : lines.size() - 1), newLine);
                    }
                    this.properties.clear();

                    /* generate a new node and replace the old one */
                    final String newContent = String.join("\n", lines);
                    PsiElement newBlock = PhpPsiElementFactory.createFromText(project, PhpDocComment.class,
                            newContent);
                    if (null != newBlock) {
                        previous.replace(newBlock);
                    }
                    lines.clear();
                }
            }
        }
    }

    public JComponent createOptionsPanel() {
        return (new MissingPropertyAnnotationsInspector.OptionsPanel()).getComponent();
    }

    private class OptionsPanel {
        final private JPanel optionsPanel;

        final private JCheckBox reportNonAsciiCodes;

        OptionsPanel() {
            optionsPanel = new JPanel();
            optionsPanel.setLayout(new MigLayout());

            reportNonAsciiCodes = new JCheckBox("Analyze only complimentary get/set methods",
                    REQUIRE_BOTH_GETTER_SETTER);
            reportNonAsciiCodes
                    .addChangeListener(e -> REQUIRE_BOTH_GETTER_SETTER = reportNonAsciiCodes.isSelected());
            optionsPanel.add(reportNonAsciiCodes, "wrap");
        }

        JPanel getComponent() {
            return optionsPanel;
        }
    }
}