Fix test of conditional operator intention

This commit is contained in:
Dmitry Jemerov 2018-08-02 16:30:24 +02:00
parent 08a65f735c
commit 441cea6a76
5 changed files with 130 additions and 72 deletions

View File

@ -1,11 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module type="PLUGIN_MODULE" version="4"> <module type="PLUGIN_MODULE" version="4">
<component name="DevKit.ModuleBuildProperties" url="file://$MODULE_DIR$/source/META-INF/plugin.xml" /> <component name="DevKit.ModuleBuildProperties" url="file://$MODULE_DIR$/META-INF/plugin.xml" />
<component name="NewModuleRootManager" inherit-compiler-output="true"> <component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output /> <exclude-output />
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/source" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/source" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/testSource" isTestSource="true" /> <sourceFolder url="file://$MODULE_DIR$/testSource" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/testData" type="java-test-resource" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />

View File

@ -1,35 +0,0 @@
<idea-plugin>
<id>com.your.company.unique.plugin.id</id>
<name>Plugin display name here</name>
<version>1.0</version>
<vendor email="support@yourcompany.com" url="http://www.yourcompany.com">YourCompany</vendor>
<description><![CDATA[
Enter short description for your plugin here.<br>
<em>most HTML tags may be used</em>
]]></description>
<change-notes><![CDATA[
Add change notes here.<br>
<em>most HTML tags may be used</em>
]]>
</change-notes>
<!-- please see http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/build_number_ranges.html for description -->
<idea-version since-build="141.0"/>
<!-- please see http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/plugin_compatibility.html
on how to target different products -->
<!-- uncomment to enable plugin in all products
<depends>com.intellij.modules.lang</depends>
-->
<extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
</extensions>
<actions>
<!-- Add your actions here -->
</actions>
</idea-plugin>

View File

@ -1,3 +1,7 @@
/*
* Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
*/
package com.intellij.codeInsight.intention; package com.intellij.codeInsight.intention;
import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.Editor;
@ -9,59 +13,115 @@ import com.intellij.util.IncorrectOperationException;
import org.jetbrains.annotations.*; import org.jetbrains.annotations.*;
/** /**
* Implements an intention action to replace a ternary statement with if-then-else
*
* @author dsl * @author dsl
*/ */
@NonNls @NonNls
public class ConditionalOperatorConvertor extends PsiElementBaseIntentionAction implements IntentionAction { public class ConditionalOperatorConvertor extends PsiElementBaseIntentionAction implements IntentionAction {
/**
* If this action is applicable, returns the text to be shown in the list of
* intention actions available.
*/
@NotNull @NotNull
public String getText() { public String getText() {
return "Convert ternary operator to if statement"; return "Convert ternary operator to if statement";
} }
/**
* Returns text for name of this family of intentions. It is used to externalize
* "auto-show" state of intentions. Only one intention action is being provided,
* so the family name is the same text as the intention action list entry.
*
* @return the intention family name.
* @see ConditionalOperatorConvertor#getText()
*/
@NotNull @NotNull
public String getFamilyName() { public String getFamilyName() {
return getText(); return getText();
} }
public boolean isAvailable(@NotNull Project project, Editor editor, @Nullable PsiElement element) {
if (element == null) return false;
if (!element.isWritable()) return false;
/**
* Checks whether this intention is available at the caret offset in file - the caret
* must sit just before a "?" character in a ternary statement. If this condition is met,
* this intention's entry is shown in the available intentions list.
*
* Note: this method must do its checks quickly and return.
*
* @param project a reference to the Project object being edited.
* @param editor a reference to the object editing the project source
* @param element a reference to the PSI element currently under the caret
* @return
* <ul>
* <li> true if the caret is in a literal string element, so this functionality
* should be added to the intention menu.</li>
* <li> false for all other types of caret positions</li>
* </ul>
*/
public boolean isAvailable(@NotNull Project project, Editor editor, @Nullable PsiElement element) {
// Quick sanity check
if (element == null) return false;
// Is this a token of type representing a "?" character?
if (element instanceof PsiJavaToken) { if (element instanceof PsiJavaToken) {
final PsiJavaToken token = (PsiJavaToken) element; final PsiJavaToken token = (PsiJavaToken) element;
if (token.getTokenType() != JavaTokenType.QUEST) return false; if (token.getTokenType() != JavaTokenType.QUEST) return false;
// Is this token part of a fully formed conditional, i.e. a ternary?
if (token.getParent() instanceof PsiConditionalExpression) { if (token.getParent() instanceof PsiConditionalExpression) {
final PsiConditionalExpression conditionalExpression = (PsiConditionalExpression) token.getParent(); final PsiConditionalExpression conditionalExpression = (PsiConditionalExpression) token.getParent();
if (conditionalExpression.getThenExpression() == null if (conditionalExpression.getThenExpression() == null
|| conditionalExpression.getElseExpression() == null) { || conditionalExpression.getElseExpression() == null) {
return false; return false;
} }
return true; return true; // Satisfies all criteria; call back invoke method
} }
return false; return false;
} }
return false; return false;
} }
/**
* Modifies the Psi to change a ternary expression to an if-then-else statement.
* If the ternary is part of a declaration, the declaration is separated and
* moved above the if-then-else statement. Called when user selects this intention action
* from the available intentions list.
*
* @param project a reference to the Project object being edited.
* @param editor a reference to the object editing the project source
* @param element a reference to the PSI element currently under the caret
* @throws IncorrectOperationException Thrown by underlying (Psi model) write action context
* when manipulation of the psi tree fails.
* @see ConditionalOperatorConvertor#startInWriteAction()
*/
public void invoke(@NotNull Project project, Editor editor, PsiElement element) throws IncorrectOperationException { public void invoke(@NotNull Project project, Editor editor, PsiElement element) throws IncorrectOperationException {
final int offset = editor.getCaretModel().getOffset();
PsiConditionalExpression conditionalExpression = PsiTreeUtil.getParentOfType(element, // Get the factory for making new PsiElements, and the code style manager to format new statements
PsiConditionalExpression.class, false); final PsiElementFactory factory = JavaPsiFacade.getInstance(project).getElementFactory();
final CodeStyleManager codeStylist = CodeStyleManager.getInstance(project);
// Get the parent of the "?" element in the ternary statement to find the conditional expression that contains it
PsiConditionalExpression conditionalExpression = PsiTreeUtil.getParentOfType(element, PsiConditionalExpression.class, false);
// Verify the conditional expression exists and has two outcomes in the ternary statement.
if (conditionalExpression == null) return; if (conditionalExpression == null) return;
if (conditionalExpression.getThenExpression() == null || conditionalExpression.getElseExpression() == null) return; if (conditionalExpression.getThenExpression() == null || conditionalExpression.getElseExpression() == null) return;
final PsiElementFactory factory = JavaPsiFacade.getInstance(project).getElementFactory(); // Keep searching up the Psi Tree in case the ternary is part of a FOR statement.
PsiElement originalStatement = PsiTreeUtil.getParentOfType(conditionalExpression, PsiStatement.class, false); PsiElement originalStatement = PsiTreeUtil.getParentOfType(conditionalExpression, PsiStatement.class, false);
while (originalStatement instanceof PsiForStatement) { while (originalStatement instanceof PsiForStatement) {
originalStatement = PsiTreeUtil.getParentOfType(originalStatement, PsiStatement.class, true); originalStatement = PsiTreeUtil.getParentOfType(originalStatement, PsiStatement.class, true);
} }
if (originalStatement == null) return; if (originalStatement == null) return;
// Maintain declrations // If the original statement is a declaration based on a ternary operator,
// split the declaration and assignment
if (originalStatement instanceof PsiDeclarationStatement) { if (originalStatement instanceof PsiDeclarationStatement) {
final PsiDeclarationStatement declaration = (PsiDeclarationStatement) originalStatement; final PsiDeclarationStatement declaration = (PsiDeclarationStatement) originalStatement;
// Find the local variable within the declaration statement
final PsiElement[] declaredElements = declaration.getDeclaredElements(); final PsiElement[] declaredElements = declaration.getDeclaredElements();
PsiLocalVariable variable = null; PsiLocalVariable variable = null;
for (PsiElement declaredElement : declaredElements) { for (PsiElement declaredElement : declaredElements) {
@ -72,38 +132,73 @@ public class ConditionalOperatorConvertor extends PsiElementBaseIntentionAction
} }
} }
if (variable == null) return; if (variable == null) return;
// Ensure that the variable declaration is not combined with other declarations, and add a mark
variable.normalizeDeclaration(); variable.normalizeDeclaration();
final Object marker = new Object(); final Object marker = new Object();
PsiTreeUtil.mark(conditionalExpression, marker); PsiTreeUtil.mark(conditionalExpression, marker);
// Create a new expression to declare the local variable
PsiExpressionStatement statement = PsiExpressionStatement statement =
(PsiExpressionStatement) factory.createStatementFromText(variable.getName() + " = 0;", null); (PsiExpressionStatement) factory.createStatementFromText(variable.getName() + " = 0;", null);
statement = (PsiExpressionStatement) CodeStyleManager.getInstance(project).reformat(statement); statement = (PsiExpressionStatement) codeStylist.reformat(statement);
// Replace initializer with the ternary expression, making an assignment statement using the ternary
((PsiAssignmentExpression) statement.getExpression()).getRExpression().replace(variable.getInitializer()); ((PsiAssignmentExpression) statement.getExpression()).getRExpression().replace(variable.getInitializer());
// Remove the initializer portion of the local variable statement,
// making it a declaration statement with no initializer
variable.getInitializer().delete(); variable.getInitializer().delete();
// Get the grandparent of the local var declaration, and add the new declaration just beneath it
final PsiElement variableParent = variable.getParent(); final PsiElement variableParent = variable.getParent();
originalStatement = variableParent.getParent().addAfter(statement, variableParent); originalStatement = variableParent.getParent().addAfter(statement, variableParent);
conditionalExpression = (PsiConditionalExpression) PsiTreeUtil.releaseMark(originalStatement, marker); conditionalExpression = (PsiConditionalExpression) PsiTreeUtil.releaseMark(originalStatement, marker);
} }
// create then and else branches // Create an IF statement from a string with placeholder elements.
final PsiElement[] originalElements = new PsiElement[]{originalStatement, conditionalExpression}; // This will replace the ternary statement
final PsiExpression condition = (PsiExpression) conditionalExpression.getCondition().copy(); PsiIfStatement newIfStmt = (PsiIfStatement) factory.createStatementFromText("if (true) {a=b;} else {c=d;}",null);
final PsiElement[] thenElements = PsiTreeUtil.copyElements(originalElements); newIfStmt = (PsiIfStatement) codeStylist.reformat(newIfStmt);
final PsiElement[] elseElements = PsiTreeUtil.copyElements(originalElements);
thenElements[1].replace(conditionalExpression.getThenExpression());
elseElements[1].replace(conditionalExpression.getElseExpression());
PsiIfStatement statement = (PsiIfStatement) factory.createStatementFromText("if (true) { a = b } else { c = d }", // Replace the conditional expression with the one from the original ternary expression
null); final PsiReferenceExpression condition = (PsiReferenceExpression) conditionalExpression.getCondition().copy();
statement = (PsiIfStatement) CodeStyleManager.getInstance(project).reformat(statement); newIfStmt.getCondition().replace(condition);
statement.getCondition().replace(condition);
statement = (PsiIfStatement) originalStatement.replace(statement); // Begin building the assignment string for the THEN and ELSE clauses using the
// parent of the ternary conditional expression
PsiAssignmentExpression assignmentExpression = PsiTreeUtil.getParentOfType(conditionalExpression, PsiAssignmentExpression.class, false);
// Get the contents of the assignment expression up to the start of the ternary expression
String exprFrag = assignmentExpression.getLExpression().getText() + assignmentExpression.getOperationSign().getText() ;
// Build the THEN statement string for the new IF statement,
// make a PsiExpressionStatement from the string, and switch the placeholder
String thenStr = exprFrag + conditionalExpression.getThenExpression().getText() + ";" ;
PsiExpressionStatement thenStmt = (PsiExpressionStatement) factory.createStatementFromText(thenStr, null);
( (PsiBlockStatement) newIfStmt.getThenBranch() ).getCodeBlock().getStatements()[0].replace(thenStmt);
// Build the ELSE statement string for the new IF statement,
// make a PsiExpressionStatement from the string, and switch the placeholder
String elseStr = exprFrag + conditionalExpression.getElseExpression().getText() + ";" ;
PsiExpressionStatement elseStmt = (PsiExpressionStatement) factory.createStatementFromText(elseStr, null);
( (PsiBlockStatement) newIfStmt.getElseBranch() ).getCodeBlock().getStatements()[0].replace(elseStmt);
// Replace the entire original statement with the new IF
newIfStmt = (PsiIfStatement) originalStatement.replace(newIfStmt);
((PsiBlockStatement) statement.getThenBranch()).getCodeBlock().getStatements()[0].replace(thenElements[0]);
((PsiBlockStatement) statement.getElseBranch()).getCodeBlock().getStatements()[0].replace(elseElements[0]);
} }
public boolean startInWriteAction() { /**
return true; * Indicates this intention action expects the Psi framework to provide the write action
} * context for any changes.
*
* @return <ul>
* <li> true if the intention requires a write action context to be provided</li>
* <li> false if this intention action will start a write action</li>
* </ul>
*/
public boolean startInWriteAction() {return true;}
} }

View File

@ -1,6 +1,7 @@
package testPlugin; package testPlugin;
import com.intellij.codeInsight.intention.IntentionAction; import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.openapi.application.PathManager;
import com.intellij.testFramework.UsefulTestCase; import com.intellij.testFramework.UsefulTestCase;
import com.intellij.testFramework.builders.JavaModuleFixtureBuilder; import com.intellij.testFramework.builders.JavaModuleFixtureBuilder;
import com.intellij.testFramework.fixtures.*; import com.intellij.testFramework.fixtures.*;
@ -17,10 +18,7 @@ import org.junit.*;
public class YourTest extends UsefulTestCase { public class YourTest extends UsefulTestCase {
protected CodeInsightTestFixture myFixture; protected CodeInsightTestFixture myFixture;
// Specify path to your test data final String dataPath = PathManager.getResourceRoot(YourTest.class, "/testPlugin/YourTest.class");
// e.g. final String dataPath = "c:\\users\\john
// .doe\\idea\\community\\samples\\conditionalOperatorConvertor/testData";
final String dataPath = "c:\\users\\John.Doe\\idea\\community\\samples\\conditionalOperatorConvertor/testData";
@Before @Before
@ -41,10 +39,9 @@ public class YourTest extends UsefulTestCase {
@After @After
public void tearDown() throws Exception { public void tearDown() throws Exception {
myFixture.tearDown(); myFixture.tearDown();
myFixture = null;
} }
protected void doTest(String testName, String hint) throws Throwable { protected void doTest(String testName, String hint) {
// Messages.showInfoMessage("Test started", "Info"); // Messages.showInfoMessage("Test started", "Info");
myFixture.configureByFile(testName + ".java"); myFixture.configureByFile(testName + ".java");
final IntentionAction action = myFixture.findSingleIntention(hint); final IntentionAction action = myFixture.findSingleIntention(hint);
@ -54,7 +51,7 @@ public class YourTest extends UsefulTestCase {
} }
@Test @Test
public void test() throws Throwable { public void test() {
doTest("before.template", "Convert ternary operator to if statement"); doTest("before.template", "Convert ternary operator to if statement");
} }