package com.intellij.codeInspection; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.psi.*; import com.intellij.psi.tree.IElementType; import com.intellij.ui.DocumentAdapter; import com.intellij.util.IncorrectOperationException; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import javax.swing.*; import javax.swing.event.DocumentEvent; import java.awt.*; import java.util.StringTokenizer; import static com.siyeh.ig.psiutils.ExpressionUtils.isNullLiteral; /** * @author max * @author jhake */ public class ComparingReferencesInspection extends AbstractBaseJavaLocalInspectionTool { private static final Logger LOG = Logger.getInstance("#com.intellij.codeInspection.ComparingReferencesInspection"); private final CriQuickFix myQuickFix = new CriQuickFix(); // Defines the text of the quick fix intention public static final String QUICK_FIX_NAME = "SDK: " + InspectionsBundle.message("inspection.comparing.references.use.quickfix"); // This string holds a list of classes relevant to this inspection. @SuppressWarnings({"WeakerAccess"}) @NonNls public String CHECKED_CLASSES = "java.lang.String;java.util.Date"; /** * This method is called to get the panel describing the inspection. * It is called every time the user selects the inspection in preferences. * The user has the option to edit the list of CHECKED_CLASSES. * Adds a document listener to see if * * @return panel to display inspection information. */ @Override public JComponent createOptionsPanel() { JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT)); final JTextField checkedClasses = new JTextField(CHECKED_CLASSES); checkedClasses.getDocument().addDocumentListener(new DocumentAdapter() { public void textChanged(DocumentEvent event) { CHECKED_CLASSES = checkedClasses.getText(); } }); panel.add(checkedClasses); return panel; } /** * This method is overridden to provide a custom visitor * that inspects expressions with relational operators '==' and '!=' * The visitor must not be recursive and must be thread-safe. * * @param holder object for visitor to register problems found. * @param isOnTheFly true if inspection was run in non-batch mode * @return non-null visitor for this inspection. * @see JavaElementVisitor */ @NotNull @Override public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder, boolean isOnTheFly) { return new JavaElementVisitor() { /** * This string defines the short message shown to a user signaling the inspection * found a problem. It reuses a string from the inspections bundle. */ @NonNls private final String DESCRIPTION_TEMPLATE = "SDK " + InspectionsBundle.message("inspection.comparing.references.problem.descriptor"); /** * Avoid defining visitors for both Reference and Binary expressions. * * @param psiReferenceExpression The expression to be evaluated. */ @Override public void visitReferenceExpression(PsiReferenceExpression psiReferenceExpression) { } /** * Evaluate binary psi expressions to see if they contain * relational operators '==' and '!=', AND they contain * classes contained in CHECKED_CLASSES. The evaluation * ignores expressions comparing an object to null. * IF this criteria is met, add the expression to the * problems list. * * @param expression The binary expression to be evaluated. */ @Override public void visitBinaryExpression(PsiBinaryExpression expression) { super.visitBinaryExpression(expression); IElementType opSign = expression.getOperationTokenType(); if (opSign == JavaTokenType.EQEQ || opSign == JavaTokenType.NE) { // The binary expression is the correct type for this inspection PsiExpression lOperand = expression.getLOperand(); PsiExpression rOperand = expression.getROperand(); if (rOperand == null || isNullLiteral(lOperand) || isNullLiteral(rOperand)) return; // Nothing is compared to null, now check the types being compared PsiType lType = lOperand.getType(); PsiType rType = rOperand.getType(); if (isCheckedType(lType) || isCheckedType(rType)) { // Identified an expression with potential problems, add to list with fix object. holder.registerProblem(expression, DESCRIPTION_TEMPLATE, myQuickFix); } } } /** * Verifies the input is the correct {@code PsiType} for this inspection. * * @param type The {@code PsiType} to be examined for a match * @return {@code true} if input is {@code PsiClassType} and matches * one of the classes in the CHECKED_CLASSES list. */ private boolean isCheckedType(PsiType type) { if (!(type instanceof PsiClassType)) return false; StringTokenizer tokenizer = new StringTokenizer(CHECKED_CLASSES, ";"); while (tokenizer.hasMoreTokens()) { String className = tokenizer.nextToken(); if (type.equalsToText(className)) return true; } return false; } }; } /** * This class provides a solution to inspection problem expressions by manipulating * the PSI tree to use a.equals(b) instead of '==' or '!=' */ private static class CriQuickFix implements LocalQuickFix { /** * Returns a partially localized string for the quick fix intention. * Used by the test code for this plugin. * * @return Quick fix short name. */ @NotNull @Override public String getName() { return QUICK_FIX_NAME; } /** * This method manipulates the PSI tree to replace 'a==b' with 'a.equals(b) * or 'a!=b' with '!a.equals(b)' * * @param project The project that contains the file being edited. * @param descriptor A problem found by this inspection. */ public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { try { PsiBinaryExpression binaryExpression = (PsiBinaryExpression) descriptor.getPsiElement(); IElementType opSign = binaryExpression.getOperationTokenType(); PsiExpression lExpr = binaryExpression.getLOperand(); PsiExpression rExpr = binaryExpression.getROperand(); if (rExpr == null) return; PsiElementFactory factory = JavaPsiFacade.getInstance(project).getElementFactory(); PsiMethodCallExpression equalsCall = (PsiMethodCallExpression) factory.createExpressionFromText("a.equals(b)", null); equalsCall.getMethodExpression().getQualifierExpression().replace(lExpr); equalsCall.getArgumentList().getExpressions()[0].replace(rExpr); PsiExpression result = (PsiExpression) binaryExpression.replace(equalsCall); if (opSign == JavaTokenType.NE) { PsiPrefixExpression negation = (PsiPrefixExpression) factory.createExpressionFromText("!a", null); negation.getOperand().replace(result); result.replace(negation); } } catch (IncorrectOperationException e) { LOG.error(e); } } @NotNull public String getFamilyName() { return getName(); } } }