From 4d1b6e67894ea3effc405b8a775a6b688d308e09 Mon Sep 17 00:00:00 2001 From: Karol Lewandowski Date: Mon, 27 Feb 2023 15:36:02 +0100 Subject: [PATCH] New topic: Inspection options (#988) --- README.md | 44 ++-- _gradleCompositeBuild/settings.gradle.kts | 2 +- .../ComparingReferencesInspection.java | 203 ------------------ .../ComparingReferences.html | 12 -- .../src/test/testData/Eq.after.java | 6 - .../src/test/testData/Eq.java | 6 - .../src/test/testData/Neq.after.java | 6 - .../src/test/testData/Neq.java | 6 - .../README.md | 10 +- .../build.gradle.kts | 8 +- .../gradle/wrapper/gradle-wrapper.jar | Bin .../gradle/wrapper/gradle-wrapper.properties | 0 .../gradlew | 0 .../gradlew.bat | 0 .../settings.gradle.kts | 2 +- .../ComparingStringReferencesInspection.java | 139 ++++++++++++ .../sdk/codeInspection/InspectionBundle.java | 27 +++ .../src/main/resources/META-INF/plugin.xml | 34 +-- .../main/resources/META-INF/pluginIcon.svg | 0 .../ComparingStringReferences.html | 9 + .../messages/InspectionBundle.properties | 5 + ...mparingStringReferencesInspectionTest.java | 68 +++--- .../src/test/testData/Eq.after.java | 5 + .../src/test/testData/Eq.java | 5 + .../src/test/testData/Neq.after.java | 5 + .../src/test/testData/Neq.java | 5 + 26 files changed, 285 insertions(+), 322 deletions(-) delete mode 100644 comparing_references_inspection/src/main/java/org/intellij/sdk/codeInspection/ComparingReferencesInspection.java delete mode 100644 comparing_references_inspection/src/main/resources/inspectionDescriptions/ComparingReferences.html delete mode 100644 comparing_references_inspection/src/test/testData/Eq.after.java delete mode 100644 comparing_references_inspection/src/test/testData/Eq.java delete mode 100644 comparing_references_inspection/src/test/testData/Neq.after.java delete mode 100644 comparing_references_inspection/src/test/testData/Neq.java rename {comparing_references_inspection => comparing_string_references_inspection}/README.md (65%) rename {comparing_references_inspection => comparing_string_references_inspection}/build.gradle.kts (71%) rename {comparing_references_inspection => comparing_string_references_inspection}/gradle/wrapper/gradle-wrapper.jar (100%) rename {comparing_references_inspection => comparing_string_references_inspection}/gradle/wrapper/gradle-wrapper.properties (100%) rename {comparing_references_inspection => comparing_string_references_inspection}/gradlew (100%) rename {comparing_references_inspection => comparing_string_references_inspection}/gradlew.bat (100%) rename {comparing_references_inspection => comparing_string_references_inspection}/settings.gradle.kts (67%) create mode 100644 comparing_string_references_inspection/src/main/java/org/intellij/sdk/codeInspection/ComparingStringReferencesInspection.java create mode 100644 comparing_string_references_inspection/src/main/java/org/intellij/sdk/codeInspection/InspectionBundle.java rename {comparing_references_inspection => comparing_string_references_inspection}/src/main/resources/META-INF/plugin.xml (59%) rename {comparing_references_inspection => comparing_string_references_inspection}/src/main/resources/META-INF/pluginIcon.svg (100%) create mode 100644 comparing_string_references_inspection/src/main/resources/inspectionDescriptions/ComparingStringReferences.html create mode 100644 comparing_string_references_inspection/src/main/resources/messages/InspectionBundle.properties rename comparing_references_inspection/src/test/java/org/intellij/sdk/codeInspection/ComparingReferencesInspectionTest.java => comparing_string_references_inspection/src/test/java/org/intellij/sdk/codeInspection/ComparingStringReferencesInspectionTest.java (60%) create mode 100644 comparing_string_references_inspection/src/test/testData/Eq.after.java create mode 100644 comparing_string_references_inspection/src/test/testData/Eq.java create mode 100644 comparing_string_references_inspection/src/test/testData/Neq.after.java create mode 100644 comparing_string_references_inspection/src/test/testData/Neq.java diff --git a/README.md b/README.md index 3f8e7f14f..4bf66339d 100644 --- a/README.md +++ b/README.md @@ -34,28 +34,28 @@ Please see [Code Samples][docs:code-samples] topic on how to import and run code In the following table, you may find all available samples provided in the separated directories as stand-alone projects available for running with the Gradle [`runIde`](tools_gradle_intellij_plugin.md#tasks-runide) task. -| Code Sample | Description | -|------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [Action Basics](./action_basics) | Action and Action Group patterns implementation, adds entries to the Tools menu. | -| [Comparing References Inspection](./comparing_references_inspection) | Local Inspection Tool, adds entries to **Settings | Editor | Inspections | Java | Probable Bugs**. | -| [Conditional Operator Intention](./conditional_operator_intention) | Intention action, suggests converting a ternary operator into an `if` block and adds entry to **Settings | Editor | Intentions | SDK Intentions**. | -| [Editor Basics](./editor_basics) | Basic Editor APIs example with editor popup menu with extra actions. | -| [Framework Basics](./framework_basics) | Basic *SDK Demo Framework* support added to the **File | New | Project | Java** wizard. | -| [Kotlin Demo](./kotlin_demo) | Kotlin example extending the *Main Menu* with a **Greeting** menu group. | -| [Live Templates](./live_templates) | Live templates for Markdown language, adds an entry to the **Settings | Editor | Live Templates** dialog. | -| [Max Opened Projects](./max_opened_projects) | Application services and listeners, shows warning dialog when more than 3 open projects are opened. | -| [Module](./module) | *SDK Demo Module* module type added to the **File | New | Project...** wizard. | -| [Product Specific - PyCharm Sample](./product_specific/pycharm_basics) | Plugin project configuration for the PyCharm IDE. | -| [Project Model](./project_model) | Interacts with the project model, adds menu items to **Tools** and **Editor Context** menus. | -| [Project View Pane](./project_view_pane) | Project View Pane listing only image files. | -| [Project Wizard](./project_wizard) | Project Wizard example with demo steps. | -| [PSI Demo](./psi_demo) | PSI Navigation features presentation. | -| [Run Configuration](./run_configuration) | Run configuration implementation with factory, options and UI. | -| [Settings](./settings) | Custom settings panel, adds a settings panel to the **Settings** panel under **Tools**. | -| [Simple Language Plugin](./simple_language_plugin) | Custom language support, defines a new *Simple language* with syntax highlighting, annotations, code completion, and other features. | -| [Theme Basics](./theme_basics) | Sample *theme* plugin with basic interface modifications. | -| [Tool Window](./tool_window) | Custom Tool Window example plugin. | -| [Tree Structure Provider](./tree_structure_provider) | Tree Structure Provider showing only plain text files. | +| Code Sample | Description | +|-----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [Action Basics](./action_basics) | Action and Action Group patterns implementation, adds entries to the Tools menu. | +| [Comparing References Inspection](./comparing_string_references_inspection) | Local Inspection Tool, adds entries to **Settings | Editor | Inspections | Java | Probable Bugs**. | +| [Conditional Operator Intention](./conditional_operator_intention) | Intention action, suggests converting a ternary operator into an `if` block and adds entry to **Settings | Editor | Intentions | SDK Intentions**. | +| [Editor Basics](./editor_basics) | Basic Editor APIs example with editor popup menu with extra actions. | +| [Framework Basics](./framework_basics) | Basic *SDK Demo Framework* support added to the **File | New | Project | Java** wizard. | +| [Kotlin Demo](./kotlin_demo) | Kotlin example extending the *Main Menu* with a **Greeting** menu group. | +| [Live Templates](./live_templates) | Live templates for Markdown language, adds an entry to the **Settings | Editor | Live Templates** dialog. | +| [Max Opened Projects](./max_opened_projects) | Application services and listeners, shows warning dialog when more than 3 open projects are opened. | +| [Module](./module) | *SDK Demo Module* module type added to the **File | New | Project...** wizard. | +| [Product Specific - PyCharm Sample](./product_specific/pycharm_basics) | Plugin project configuration for the PyCharm IDE. | +| [Project Model](./project_model) | Interacts with the project model, adds menu items to **Tools** and **Editor Context** menus. | +| [Project View Pane](./project_view_pane) | Project View Pane listing only image files. | +| [Project Wizard](./project_wizard) | Project Wizard example with demo steps. | +| [PSI Demo](./psi_demo) | PSI Navigation features presentation. | +| [Run Configuration](./run_configuration) | Run configuration implementation with factory, options and UI. | +| [Settings](./settings) | Custom settings panel, adds a settings panel to the **Settings** panel under **Tools**. | +| [Simple Language Plugin](./simple_language_plugin) | Custom language support, defines a new *Simple language* with syntax highlighting, annotations, code completion, and other features. | +| [Theme Basics](./theme_basics) | Sample *theme* plugin with basic interface modifications. | +| [Tool Window](./tool_window) | Custom Tool Window example plugin. | +| [Tree Structure Provider](./tree_structure_provider) | Tree Structure Provider showing only plain text files. | [gh:workflow-code-samples]: https://github.com/JetBrains/intellij-sdk-docs/actions/workflows/code-samples.yml [gh:template]: https://github.com/JetBrains/intellij-platform-plugin-template diff --git a/_gradleCompositeBuild/settings.gradle.kts b/_gradleCompositeBuild/settings.gradle.kts index 25a98dfd0..0f96b40ab 100644 --- a/_gradleCompositeBuild/settings.gradle.kts +++ b/_gradleCompositeBuild/settings.gradle.kts @@ -5,7 +5,7 @@ rootProject.name = "SDK Code Samples" includeBuild("../action_basics") -includeBuild("../comparing_references_inspection") +includeBuild("../comparing_string_references_inspection") includeBuild("../conditional_operator_intention") includeBuild("../editor_basics") includeBuild("../facet_basics") diff --git a/comparing_references_inspection/src/main/java/org/intellij/sdk/codeInspection/ComparingReferencesInspection.java b/comparing_references_inspection/src/main/java/org/intellij/sdk/codeInspection/ComparingReferencesInspection.java deleted file mode 100644 index 792c39882..000000000 --- a/comparing_references_inspection/src/main/java/org/intellij/sdk/codeInspection/ComparingReferencesInspection.java +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright 2000-2022 JetBrains s.r.o. and other contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. - -package org.intellij.sdk.codeInspection; - -import 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; - -/** - * Implements an inspection to detect when object references are compared using 'a==b' or 'a!=b'. - * The quick fix converts these comparisons to 'a.equals(b) or '!a.equals(b)' respectively. - */ -public class ComparingReferencesInspection extends AbstractBaseJavaLocalInspectionTool { - - // Defines the text of the quick fix intention - public static final String QUICK_FIX_NAME = "SDK: " + - InspectionsBundle.message("inspection.comparing.references.use.quickfix"); - private static final Logger LOG = Logger.getInstance("#com.intellij.codeInspection.ComparingReferencesInspection"); - private final CriQuickFix myQuickFix = new CriQuickFix(); - // 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 {@link #CHECKED_CLASSES}. - * - * @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(@NotNull 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 {@link ComparingReferencesInspection#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(); - } - - } - -} diff --git a/comparing_references_inspection/src/main/resources/inspectionDescriptions/ComparingReferences.html b/comparing_references_inspection/src/main/resources/inspectionDescriptions/ComparingReferences.html deleted file mode 100644 index 292ad9dad..000000000 --- a/comparing_references_inspection/src/main/resources/inspectionDescriptions/ComparingReferences.html +++ /dev/null @@ -1,12 +0,0 @@ - - - -Reports usages of == and != when comparing instances of specified types. -

- Quick-fix replaces operator with equals() call. -

- -

Configure the inspection:

-List classes to be inspected separated by semi-colon. - - diff --git a/comparing_references_inspection/src/test/testData/Eq.after.java b/comparing_references_inspection/src/test/testData/Eq.after.java deleted file mode 100644 index 20a37653b..000000000 --- a/comparing_references_inspection/src/test/testData/Eq.after.java +++ /dev/null @@ -1,6 +0,0 @@ -public class Eq { - public boolean compare2Strings(java.lang.String s1, java.lang.String s2) { - return (s1.equals(s2)); - } - -} \ No newline at end of file diff --git a/comparing_references_inspection/src/test/testData/Eq.java b/comparing_references_inspection/src/test/testData/Eq.java deleted file mode 100644 index 43e40a40b..000000000 --- a/comparing_references_inspection/src/test/testData/Eq.java +++ /dev/null @@ -1,6 +0,0 @@ -public class Eq { - public boolean compare2Strings(java.lang.String s1, java.lang.String s2) { - return (s1 == s2); - } - -} \ No newline at end of file diff --git a/comparing_references_inspection/src/test/testData/Neq.after.java b/comparing_references_inspection/src/test/testData/Neq.after.java deleted file mode 100644 index 440f8bf4a..000000000 --- a/comparing_references_inspection/src/test/testData/Neq.after.java +++ /dev/null @@ -1,6 +0,0 @@ -public class Neq { -public boolean compare2Dates(java.util.Date dt1, java.util.Date dt2){ - return (!dt1.equals(dt2)); - } - -} \ No newline at end of file diff --git a/comparing_references_inspection/src/test/testData/Neq.java b/comparing_references_inspection/src/test/testData/Neq.java deleted file mode 100644 index aa7a04f22..000000000 --- a/comparing_references_inspection/src/test/testData/Neq.java +++ /dev/null @@ -1,6 +0,0 @@ -public class Neq { -public boolean compare2Dates(java.util.Date dt1, java.util.Date dt2){ - return (dt1 != dt2); - } - -} \ No newline at end of file diff --git a/comparing_references_inspection/README.md b/comparing_string_references_inspection/README.md similarity index 65% rename from comparing_references_inspection/README.md rename to comparing_string_references_inspection/README.md index fc6580ca3..719c54405 100644 --- a/comparing_references_inspection/README.md +++ b/comparing_string_references_inspection/README.md @@ -5,14 +5,14 @@ Comparing References Inspection Sample demonstrates the implementation of the [Code Inspections][docs:code_inspections] feature for Java classes. -The plugin inspects your Java code and highlights any fragments containing the comparison of two `String` or `Date` variables. +The plugin inspects your Java code and highlights any fragments containing the comparison of two `String` variables. If such a check finds a comparison using the `==` or !`=` operators instead of the `.equals()` method, the plugin proposes a *quick-fix* action. ### Extension Points -| Name | Implementation | Extension Point Class | -|--------------------------------|---------------------------------------------------------------------|---------------------------------------| -| `com.intellij.localInspection` | [ComparingReferencesInspection][file:ComparingReferencesInspection] | `AbstractBaseJavaLocalInspectionTool` | +| Name | Implementation | Extension Point Class | +|--------------------------------|---------------------------------------------------------------------------------|---------------------------------------| +| `com.intellij.localInspection` | [ComparingStringReferencesInspection][file:ComparingStringReferencesInspection] | `AbstractBaseJavaLocalInspectionTool` | *Reference: [Plugin Extension Points in IntelliJ SDK Docs][docs:ep]* @@ -21,4 +21,4 @@ If such a check finds a comparison using the `==` or !`=` operators instead of t [docs:code_inspections]: https://plugins.jetbrains.com/docs/intellij/code-inspections.html [docs:ep]: https://plugins.jetbrains.com/docs/intellij/plugin-extensions.html -[file:ComparingReferencesInspection]: ./src/main/java/org/intellij/sdk/codeInspection/ComparingReferencesInspection.java +[file:ComparingStringReferencesInspection]: ./src/main/java/org/intellij/sdk/codeInspection/ComparingStringReferencesInspection.java diff --git a/comparing_references_inspection/build.gradle.kts b/comparing_string_references_inspection/build.gradle.kts similarity index 71% rename from comparing_references_inspection/build.gradle.kts rename to comparing_string_references_inspection/build.gradle.kts index c29ea8a5c..6a94a8bfa 100644 --- a/comparing_references_inspection/build.gradle.kts +++ b/comparing_string_references_inspection/build.gradle.kts @@ -1,4 +1,4 @@ -// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. plugins { id("java") @@ -36,10 +36,4 @@ tasks { sinceBuild.set("221") untilBuild.set("223.*") } - - test { - // Set idea.home.path to the absolute path to the intellij-community source - // on your local machine. - systemProperty("idea.home.path", "/Users/jhake/Documents/source/comm") - } } diff --git a/comparing_references_inspection/gradle/wrapper/gradle-wrapper.jar b/comparing_string_references_inspection/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from comparing_references_inspection/gradle/wrapper/gradle-wrapper.jar rename to comparing_string_references_inspection/gradle/wrapper/gradle-wrapper.jar diff --git a/comparing_references_inspection/gradle/wrapper/gradle-wrapper.properties b/comparing_string_references_inspection/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from comparing_references_inspection/gradle/wrapper/gradle-wrapper.properties rename to comparing_string_references_inspection/gradle/wrapper/gradle-wrapper.properties diff --git a/comparing_references_inspection/gradlew b/comparing_string_references_inspection/gradlew similarity index 100% rename from comparing_references_inspection/gradlew rename to comparing_string_references_inspection/gradlew diff --git a/comparing_references_inspection/gradlew.bat b/comparing_string_references_inspection/gradlew.bat similarity index 100% rename from comparing_references_inspection/gradlew.bat rename to comparing_string_references_inspection/gradlew.bat diff --git a/comparing_references_inspection/settings.gradle.kts b/comparing_string_references_inspection/settings.gradle.kts similarity index 67% rename from comparing_references_inspection/settings.gradle.kts rename to comparing_string_references_inspection/settings.gradle.kts index 9e1df8ce4..f78a8b3d8 100644 --- a/comparing_references_inspection/settings.gradle.kts +++ b/comparing_string_references_inspection/settings.gradle.kts @@ -1,3 +1,3 @@ // Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -rootProject.name = "comparing_references_inspection" +rootProject.name = "comparing_string_references_inspection" diff --git a/comparing_string_references_inspection/src/main/java/org/intellij/sdk/codeInspection/ComparingStringReferencesInspection.java b/comparing_string_references_inspection/src/main/java/org/intellij/sdk/codeInspection/ComparingStringReferencesInspection.java new file mode 100644 index 000000000..b59a399be --- /dev/null +++ b/comparing_string_references_inspection/src/main/java/org/intellij/sdk/codeInspection/ComparingStringReferencesInspection.java @@ -0,0 +1,139 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + +package org.intellij.sdk.codeInspection; + +import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool; +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.codeInspection.ProblemDescriptor; +import com.intellij.codeInspection.ProblemsHolder; +import com.intellij.openapi.project.Project; +import com.intellij.psi.*; +import com.intellij.psi.tree.IElementType; +import org.jetbrains.annotations.NotNull; + +import static com.siyeh.ig.psiutils.ExpressionUtils.isNullLiteral; + +/** + * Implements an inspection to detect when String references are compared using 'a==b' or 'a!=b'. + * The quick fix converts these comparisons to 'a.equals(b) or '!a.equals(b)' respectively. + */ +public class ComparingStringReferencesInspection extends AbstractBaseJavaLocalInspectionTool { + + private final ReplaceWithEqualsQuickFix myQuickFix = new ReplaceWithEqualsQuickFix(); + + /** + * 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 the 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() { + + /** + * Evaluate binary psi expressions to see if they contain relational operators '==' and '!=', + * AND they are of String type. + * The evaluation ignores expressions comparing an object to null. + * IF these criteria are met, register the problem in the ProblemsHolder. + * + * @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 + if (isStringType(lOperand) || isStringType(rOperand)) { + // Identified an expression with potential problems, register problem with the quick fix object + holder.registerProblem(expression, + InspectionBundle.message("inspection.comparing.string.references.problem.descriptor"), + myQuickFix); + } + } + } + + private boolean isStringType(PsiExpression operand) { + PsiType type = operand.getType(); + if (!(type instanceof PsiClassType)) { + return false; + } + PsiClass resolvedType = ((PsiClassType) type).resolve(); + if (resolvedType == null) { + return false; + } + return "java.lang.String".equals(resolvedType.getQualifiedName()); + } + + }; + } + + /** + * 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 ReplaceWithEqualsQuickFix 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 InspectionBundle.message("inspection.comparing.string.references.use.quickfix"); + } + + /** + * 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) { + 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); + } + } + + @NotNull + public String getFamilyName() { + return getName(); + } + + } + +} diff --git a/comparing_string_references_inspection/src/main/java/org/intellij/sdk/codeInspection/InspectionBundle.java b/comparing_string_references_inspection/src/main/java/org/intellij/sdk/codeInspection/InspectionBundle.java new file mode 100644 index 000000000..f3fbcec52 --- /dev/null +++ b/comparing_string_references_inspection/src/main/java/org/intellij/sdk/codeInspection/InspectionBundle.java @@ -0,0 +1,27 @@ +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + +package org.intellij.sdk.codeInspection; + +import com.intellij.DynamicBundle; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.PropertyKey; + +public final class InspectionBundle extends DynamicBundle { + + private static final InspectionBundle ourInstance = new InspectionBundle(); + + @NonNls + public static final String BUNDLE = "messages.InspectionBundle"; + + private InspectionBundle() { + super(BUNDLE); + } + + public static @Nls String message(@NotNull @PropertyKey(resourceBundle = BUNDLE) String key, + Object @NotNull ... params) { + return ourInstance.getMessage(key, params); + } + +} diff --git a/comparing_references_inspection/src/main/resources/META-INF/plugin.xml b/comparing_string_references_inspection/src/main/resources/META-INF/plugin.xml similarity index 59% rename from comparing_references_inspection/src/main/resources/META-INF/plugin.xml rename to comparing_string_references_inspection/src/main/resources/META-INF/plugin.xml index 99358e841..bc0938d7f 100644 --- a/comparing_references_inspection/src/main/resources/META-INF/plugin.xml +++ b/comparing_string_references_inspection/src/main/resources/META-INF/plugin.xml @@ -1,4 +1,4 @@ - + @@ -35,37 +35,37 @@ + implementationClass="org.intellij.sdk.codeInspection.ComparingStringReferencesInspection"/> diff --git a/comparing_references_inspection/src/main/resources/META-INF/pluginIcon.svg b/comparing_string_references_inspection/src/main/resources/META-INF/pluginIcon.svg similarity index 100% rename from comparing_references_inspection/src/main/resources/META-INF/pluginIcon.svg rename to comparing_string_references_inspection/src/main/resources/META-INF/pluginIcon.svg diff --git a/comparing_string_references_inspection/src/main/resources/inspectionDescriptions/ComparingStringReferences.html b/comparing_string_references_inspection/src/main/resources/inspectionDescriptions/ComparingStringReferences.html new file mode 100644 index 000000000..f68912c55 --- /dev/null +++ b/comparing_string_references_inspection/src/main/resources/inspectionDescriptions/ComparingStringReferences.html @@ -0,0 +1,9 @@ + + + +Reports usages of == and != when comparing instances of String. +

+ Quick-fix replaces operator with equals() call. +

+ + diff --git a/comparing_string_references_inspection/src/main/resources/messages/InspectionBundle.properties b/comparing_string_references_inspection/src/main/resources/messages/InspectionBundle.properties new file mode 100644 index 000000000..b480d8836 --- /dev/null +++ b/comparing_string_references_inspection/src/main/resources/messages/InspectionBundle.properties @@ -0,0 +1,5 @@ +# Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. + +inspection.comparing.string.references.display.name=SDK: '==' or '!=' used instead of 'equals()' +inspection.comparing.string.references.problem.descriptor=SDK: String objects compared with equality operation +inspection.comparing.string.references.use.quickfix=SDK: Use equals() diff --git a/comparing_references_inspection/src/test/java/org/intellij/sdk/codeInspection/ComparingReferencesInspectionTest.java b/comparing_string_references_inspection/src/test/java/org/intellij/sdk/codeInspection/ComparingStringReferencesInspectionTest.java similarity index 60% rename from comparing_references_inspection/src/test/java/org/intellij/sdk/codeInspection/ComparingReferencesInspectionTest.java rename to comparing_string_references_inspection/src/test/java/org/intellij/sdk/codeInspection/ComparingStringReferencesInspectionTest.java index 263a535fc..d77cd8a9d 100644 --- a/comparing_references_inspection/src/test/java/org/intellij/sdk/codeInspection/ComparingReferencesInspectionTest.java +++ b/comparing_string_references_inspection/src/test/java/org/intellij/sdk/codeInspection/ComparingStringReferencesInspectionTest.java @@ -1,23 +1,31 @@ -// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package org.intellij.sdk.codeInspection; import com.intellij.codeInsight.daemon.impl.HighlightInfo; import com.intellij.codeInsight.intention.IntentionAction; +import com.intellij.testFramework.TestDataPath; import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase; import org.jetbrains.annotations.NotNull; import java.util.List; -/** - * Class for testing ComparingReferencesInspection. - * Requires {@code idea.home.path} to be set in build.gradle.kts. - * doTest() does the work for individual test cases. - */ -public class ComparingReferencesInspectionTest extends LightJavaCodeInsightFixtureTestCase { +@TestDataPath("$CONTENT_ROOT/testData") +public class ComparingStringReferencesInspectionTest extends LightJavaCodeInsightFixtureTestCase { + + private static final String QUICK_FIX_NAME = + InspectionBundle.message("inspection.comparing.string.references.use.quickfix"); + + @Override + protected void setUp() throws Exception { + super.setUp(); + myFixture.enableInspections(new ComparingStringReferencesInspection()); + // optimization: add a fake java.lang.String class to avoid loading all JDK classes for test + myFixture.addClass("package java.lang; public final class String {}"); + } /** - * Defines path to files used for running tests. + * Defines the path to files used for running tests. * * @return The path from this module's root directory ($MODULE_WORKING_DIR$) to the * directory containing files for these tests. @@ -27,28 +35,6 @@ public class ComparingReferencesInspectionTest extends LightJavaCodeInsightFixtu return "src/test/testData"; } - /** - * Given the name of a test file, runs comparing references inspection quick fix and tests - * the results against a reference outcome file. File name pattern 'foo.java' and 'foo.after.java' - * are matching before and after files in the testData directory. - * - * @param testName The name of the test file before comparing references inspection. - */ - protected void doTest(@NotNull String testName) { - // Initialize the test based on the testData file - myFixture.configureByFile(testName + ".java"); - // Initialize the inspection and get a list of highlighted - myFixture.enableInspections(new ComparingReferencesInspection()); - List highlightInfos = myFixture.doHighlighting(); - assertFalse(highlightInfos.isEmpty()); - // Get the quick fix action for comparing references inspection and apply it to the file - final IntentionAction action = myFixture.findSingleIntention(ComparingReferencesInspection.QUICK_FIX_NAME); - assertNotNull(action); - myFixture.launchAction(action); - // Verify the results - myFixture.checkResultByFile(testName + ".after.java"); - } - /** * Test the '==' case. */ @@ -63,4 +49,26 @@ public class ComparingReferencesInspectionTest extends LightJavaCodeInsightFixtu doTest("Neq"); } + /** + * Given the name of a test file, runs comparing references inspection quick fix and tests + * the results against a reference outcome file. + * File name pattern 'foo.java' and 'foo.after.java' are matching before and after files + * in the testData directory. + * + * @param testName test file name base + */ + protected void doTest(@NotNull String testName) { + // Initialize the test based on the testData file + myFixture.configureByFile(testName + ".java"); + // Initialize the inspection and get a list of highlighted + List highlightInfos = myFixture.doHighlighting(); + assertFalse(highlightInfos.isEmpty()); + // Get the quick fix action for comparing references inspection and apply it to the file + final IntentionAction action = myFixture.findSingleIntention(QUICK_FIX_NAME); + assertNotNull(action); + myFixture.launchAction(action); + // Verify the results + myFixture.checkResultByFile(testName + ".after.java"); + } + } diff --git a/comparing_string_references_inspection/src/test/testData/Eq.after.java b/comparing_string_references_inspection/src/test/testData/Eq.after.java new file mode 100644 index 000000000..f4afd6dbc --- /dev/null +++ b/comparing_string_references_inspection/src/test/testData/Eq.after.java @@ -0,0 +1,5 @@ +public class Eq { + public boolean compareStrings(String s1, String s2) { + return (s1.equals(s2)); + } +} diff --git a/comparing_string_references_inspection/src/test/testData/Eq.java b/comparing_string_references_inspection/src/test/testData/Eq.java new file mode 100644 index 000000000..2a8e3951e --- /dev/null +++ b/comparing_string_references_inspection/src/test/testData/Eq.java @@ -0,0 +1,5 @@ +public class Eq { + public boolean compareStrings(String s1, String s2) { + return (s1 == s2); + } +} diff --git a/comparing_string_references_inspection/src/test/testData/Neq.after.java b/comparing_string_references_inspection/src/test/testData/Neq.after.java new file mode 100644 index 000000000..dafe06016 --- /dev/null +++ b/comparing_string_references_inspection/src/test/testData/Neq.after.java @@ -0,0 +1,5 @@ +public class Neq { + public boolean compareStrings(String s1, String s2) { + return (!s1.equals(s2)); + } +} diff --git a/comparing_string_references_inspection/src/test/testData/Neq.java b/comparing_string_references_inspection/src/test/testData/Neq.java new file mode 100644 index 000000000..4bf437dc0 --- /dev/null +++ b/comparing_string_references_inspection/src/test/testData/Neq.java @@ -0,0 +1,5 @@ +public class Neq { + public boolean compareStrings(String s1, String s2) { + return (s1 != s2); + } +}