Skip to content

Commit 4020132

Browse files
authored
Merge pull request #1 from githublwm/refine-add-final-intention
Refactor and test AddFinalIntention
2 parents ddbdc15 + 2ddace6 commit 4020132

File tree

2 files changed

+211
-22
lines changed

2 files changed

+211
-22
lines changed

src/main/java/lwm/plugin/intention/AddFinalIntention.java

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,25 @@
55
import com.intellij.codeInspection.util.IntentionName;
66
import com.intellij.openapi.editor.Editor;
77
import com.intellij.openapi.project.Project;
8-
import com.intellij.psi.*;
8+
import com.intellij.psi.PsiCodeBlock;
9+
import com.intellij.psi.PsiElement;
10+
import com.intellij.psi.PsiField;
11+
import com.intellij.psi.PsiFile;
12+
import com.intellij.psi.PsiJavaFile;
13+
import com.intellij.psi.PsiLocalVariable;
14+
import com.intellij.psi.PsiMethod;
15+
import com.intellij.psi.PsiModifier;
16+
import com.intellij.psi.PsiModifierList;
17+
import com.intellij.psi.PsiModifierListOwner;
18+
import com.intellij.psi.PsiParameter;
19+
import com.intellij.psi.PsiParameterList;
20+
import com.intellij.psi.PsiVariable;
921
import com.intellij.psi.util.PsiTreeUtil;
1022
import com.intellij.psi.util.PsiUtil;
1123
import com.intellij.util.IncorrectOperationException;
1224
import org.jetbrains.annotations.NotNull;
1325

14-
import java.util.Arrays;
26+
import java.util.Collection;
1527

1628
/**
1729
* @author longwm
@@ -28,36 +40,64 @@ public class AddFinalIntention implements IntentionAction {
2840
}
2941

3042
@Override
31-
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
32-
return file instanceof PsiJavaFile;
43+
public boolean isAvailable(@NotNull final Project project, final Editor editor, final PsiFile file) {
44+
if (!(file instanceof PsiJavaFile)) {
45+
return false;
46+
}
47+
final PsiElement element = file.findElementAt(editor.getCaretModel().getOffset());
48+
if (element == null) {
49+
return false;
50+
}
51+
final PsiField field = PsiTreeUtil.getParentOfType(element, PsiField.class, false);
52+
if (field != null) {
53+
return true;
54+
}
55+
if (element instanceof PsiVariable) {
56+
return true;
57+
}
58+
59+
return PsiTreeUtil.getParentOfType(element, PsiMethod.class) != null;
3360
}
3461

3562
@Override
36-
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
63+
public void invoke(@NotNull final Project project, final Editor editor, final PsiFile file) throws IncorrectOperationException {
3764
final PsiElement element = file.findElementAt(editor.getCaretModel().getOffset());
65+
if (element == null) {
66+
return;
67+
}
3868

39-
final PsiMethod method = PsiTreeUtil.getParentOfType(element, PsiMethod.class);
40-
if (method != null) {
41-
final PsiParameterList parameterList = method.getParameterList();
42-
Arrays.stream(parameterList.getParameters())
43-
.forEach(psiParameter -> PsiUtil.setModifierProperty(psiParameter, PsiModifier.FINAL, true));
44-
final PsiCodeBlock methodBody = method.getBody();
69+
final PsiVariable specificVariable = PsiTreeUtil.getParentOfType(element, PsiVariable.class, false);
70+
if (specificVariable != null) {
71+
addFinalModifierIfNotPresent(specificVariable);
72+
return;
73+
}
74+
75+
final PsiMethod containingMethod = PsiTreeUtil.getParentOfType(element, PsiMethod.class);
76+
if (containingMethod != null) {
77+
// Add final to parameters
78+
final PsiParameterList parameterList = containingMethod.getParameterList();
79+
for (final PsiParameter parameter : parameterList.getParameters()) {
80+
addFinalModifierIfNotPresent(parameter);
81+
}
82+
83+
// Add final to local variables
84+
final PsiCodeBlock methodBody = containingMethod.getBody();
4585
if (methodBody != null) {
46-
final PsiStatement[] statements = methodBody.getStatements();
47-
Arrays.stream(statements)
48-
.filter(PsiDeclarationStatement.class::isInstance)
49-
.map(psiStatement -> ((PsiDeclarationStatement) psiStatement).getDeclaredElements())
50-
.flatMap(Arrays::stream)
51-
.filter(PsiLocalVariable.class::isInstance)
52-
.forEach(psiElement -> PsiUtil.setModifierProperty((PsiLocalVariable) psiElement, PsiModifier.FINAL, true));
86+
final Collection<PsiLocalVariable> localVariables = PsiTreeUtil.collectElementsOfType(methodBody, PsiLocalVariable.class);
87+
for (final PsiLocalVariable localVariable : localVariables) {
88+
addFinalModifierIfNotPresent(localVariable);
89+
}
5390
}
5491
}
92+
}
5593

56-
final PsiVariable psiVariable = PsiTreeUtil.getParentOfType(element, PsiVariable.class);
57-
if (psiVariable != null) {
58-
PsiUtil.setModifierProperty(psiVariable, PsiModifier.FINAL, true);
94+
private void addFinalModifierIfNotPresent(final PsiModifierListOwner element) {
95+
if (element != null) {
96+
final PsiModifierList modifierList = element.getModifierList();
97+
if (modifierList != null && !modifierList.hasExplicitModifier(PsiModifier.FINAL)) {
98+
PsiUtil.setModifierProperty(element, PsiModifier.FINAL, true);
99+
}
59100
}
60-
61101
}
62102

63103
@Override
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package lwm.plugin.intention;
2+
3+
import com.intellij.codeInsight.intention.IntentionAction;
4+
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
5+
import java.util.List;
6+
import java.util.stream.Collectors;
7+
8+
public class AddFinalIntentionTest extends LightJavaCodeInsightFixtureTestCase {
9+
10+
private static final String INTENTION_TEXT = "Add final modifier(添加final修饰)";
11+
12+
private void doTest(final String before, final String after) {
13+
assertTrue("Test code must contain <caret>", before.contains("<caret>"));
14+
myFixture.configureByText("Test.java", before);
15+
final IntentionAction intention = myFixture.findSingleIntention(INTENTION_TEXT);
16+
assertNotNull("Intention '" + INTENTION_TEXT + "' not found", intention);
17+
myFixture.launchAction(intention);
18+
myFixture.checkResult(after);
19+
}
20+
21+
private void doTestNotAvailable(final String content) {
22+
assertTrue("Test code must contain <caret>", content.contains("<caret>"));
23+
myFixture.configureByText("Test.java", content);
24+
final List<IntentionAction> intentions = myFixture.getAvailableIntentions();
25+
final List<String> intentionTexts = intentions.stream()
26+
.map(IntentionAction::getText)
27+
.collect(Collectors.toList());
28+
assertFalse("Intention '" + INTENTION_TEXT + "' should not be available. Available: " + intentionTexts,
29+
intentionTexts.contains(INTENTION_TEXT));
30+
}
31+
32+
private void doTestNoChange(final String contentWithCaret) {
33+
assertTrue("Test code must contain <caret>", contentWithCaret.contains("<caret>"));
34+
final String contentWithoutCaret = contentWithCaret.replace("<caret>", "");
35+
myFixture.configureByText("Test.java", contentWithCaret);
36+
// Attempt to find the intention. It should be available.
37+
final IntentionAction intention = myFixture.findSingleIntention(INTENTION_TEXT);
38+
assertNotNull("Intention '" + INTENTION_TEXT + "' should be available but was not found.", intention);
39+
myFixture.launchAction(intention);
40+
myFixture.checkResult(contentWithoutCaret); // Ensure no change happened
41+
}
42+
43+
public void testAddFinalToSingleParameter() {
44+
final String before = "class Test {\n" +
45+
" void method(String pa<caret>ram1, int param2) {}\n" +
46+
"}";
47+
final String after = "class Test {\n" +
48+
" void method(final String param1, int param2) {}\n" +
49+
"}";
50+
doTest(before, after);
51+
}
52+
53+
public void testAddFinalToSingleLocalVariable() {
54+
final String before = "class Test {\n" +
55+
" void method() {\n" +
56+
" String l<caret>ocal1 = \"hello\";\n" +
57+
" int local2 = 10;\n" +
58+
" }\n" +
59+
"}";
60+
final String after = "class Test {\n" +
61+
" void method() {\n" +
62+
" final String local1 = \"hello\";\n" +
63+
" int local2 = 10;\n" +
64+
" }\n" +
65+
"}";
66+
doTest(before, after);
67+
}
68+
69+
public void testAddFinalToAllInMethodScope() {
70+
// Caret is removed in 'after' string by checkResult implicitly if not present.
71+
final String before = "class Test {\n" +
72+
" void method(String param1, int param2) {\n" +
73+
" <caret>\n" +
74+
" String local1 = \"hello\";\n" +
75+
" int local2 = 10;\n" +
76+
" final String alreadyFinal = \"done\";\n" +
77+
" }\n" +
78+
"}";
79+
final String after = "class Test {\n" +
80+
" void method(final String param1, final int param2) {\n" +
81+
" \n" +
82+
" final String local1 = \"hello\";\n" +
83+
" final int local2 = 10;\n" +
84+
" final String alreadyFinal = \"done\";\n" +
85+
" }\n" +
86+
"}";
87+
doTest(before, after);
88+
}
89+
90+
public void testNotAvailableOnAlreadyFinalParameter() {
91+
final String content = "class Test {\n" +
92+
" void method(final String pa<caret>ram1) {}\n" +
93+
"}";
94+
// As per current isAvailable & invoke logic, intention is available but makes no change.
95+
doTestNoChange(content);
96+
}
97+
98+
public void testNotAvailableOnAlreadyFinalLocalVariable() {
99+
final String content = "class Test {\n" +
100+
" void method() {\n" +
101+
" final String lo<caret>cal1 = \"hello\";\n" +
102+
" }\n" +
103+
"}";
104+
// As per current isAvailable & invoke logic, intention is available but makes no change.
105+
doTestNoChange(content);
106+
}
107+
108+
public void testNotAvailableOnClassDeclaration() {
109+
final String content = "class Te<caret>st {\n" +
110+
" void method(String param1) {}\n" +
111+
"}";
112+
doTestNotAvailable(content);
113+
}
114+
115+
public void testNotAvailableInImportStatement() {
116+
final String content = "import ja<caret>va.util.List;\n" +
117+
"class Test {\n" +
118+
" void method(String param1) {}\n" +
119+
"}";
120+
doTestNotAvailable(content);
121+
}
122+
123+
public void testNotAvailableOnFieldName() {
124+
// The current isAvailable will make it available for fields.
125+
// The invoke method will make the field final.
126+
final String before = "class Test {\n" +
127+
" String myFi<caret>eld = \"value\";\n" +
128+
"}";
129+
final String after = "class Test {\n" +
130+
" final String myField = \"value\";\n" +
131+
"}";
132+
doTest(before, after);
133+
}
134+
135+
public void testNotAvailableOnAlreadyFinalFieldName() {
136+
final String content = "class Test {\n" +
137+
" final String myFi<caret>eld = \"value\";\n" +
138+
"}";
139+
// Intention should be available (based on isAvailable) but do nothing (based on invoke)
140+
doTestNoChange(content);
141+
}
142+
143+
144+
@Override
145+
protected String getTestDataPath() {
146+
// Not using testData files for these tests, so path can be empty or root.
147+
return "";
148+
}
149+
}

0 commit comments

Comments
 (0)