[java-intentions] AddMethodQualifierFix: ModCommand

GitOrigin-RevId: efe77eed876481149c7b58356ffd6dcada649a16
This commit is contained in:
Tagir Valeev
2023-07-16 22:19:44 +02:00
committed by intellij-monorepo-bot
parent f5ed03ef33
commit 5d5dfb997a
11 changed files with 206 additions and 167 deletions

View File

@@ -1,80 +1,32 @@
/*
* Copyright 2000-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.daemon.impl.quickfix;
import com.intellij.codeInsight.daemon.QuickFixBundle;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInsight.intention.preview.IntentionPreviewInfo;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.PopupStep;
import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
import com.intellij.codeInsight.intention.preview.IntentionPreviewUtils;
import com.intellij.codeInspection.ModCommands;
import com.intellij.modcommand.ModChooseAction;
import com.intellij.modcommand.ModCommand;
import com.intellij.modcommand.ModCommandAction;
import com.intellij.modcommand.PsiBasedModCommandAction;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import javax.swing.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static java.util.Objects.requireNonNullElse;
/**
* @author Dmitry Batkovich
*/
public class AddMethodQualifierFix implements IntentionAction {
public class AddMethodQualifierFix extends PsiBasedModCommandAction<PsiMethodCallExpression> {
private enum SearchMode { MAX_2_CANDIDATES, FULL_SEARCH }
private final SmartPsiElementPointer<PsiMethodCallExpression> myMethodCall;
private volatile List<PsiVariable> myCandidates;
public AddMethodQualifierFix(@NotNull PsiMethodCallExpression methodCallExpression) {
myMethodCall = SmartPointerManager.getInstance(methodCallExpression.getProject()).createSmartPsiElementPointer(methodCallExpression);
}
@Override
public @NotNull IntentionPreviewInfo generatePreview(@NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {
PsiMethodCallExpression element = myMethodCall.getElement();
if (element == null || myCandidates.isEmpty()) {
return IntentionPreviewInfo.EMPTY;
}
PsiMethodCallExpression copyExpression = PsiTreeUtil.findSameElementInCopy(element, file);
replaceWithQualifier(myCandidates.get(0), copyExpression);
return IntentionPreviewInfo.DIFF;
}
@NotNull
@Override
public String getText() {
if (myCandidates == null || myCandidates.isEmpty()) {
if (ApplicationManager.getApplication().isHeadlessEnvironment()) {
return "";
}
throw new IllegalStateException();
}
if (myCandidates.size() == 1) {
return QuickFixBundle.message("add.method.qualifier.fix.text", myCandidates.get(0).getName());
}
return getFamilyName();
super(methodCallExpression);
}
@NotNull
@@ -84,23 +36,18 @@ public class AddMethodQualifierFix implements IntentionAction {
}
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile file) {
PsiMethodCallExpression element = myMethodCall.getElement();
if (element == null || !element.isValid() || element.getMethodExpression().getQualifierExpression() != null) {
return false;
protected @Nullable Presentation getPresentation(@NotNull ActionContext context, @NotNull PsiMethodCallExpression element) {
if (element.getMethodExpression().getQualifierExpression() != null) return null;
List<PsiVariable> candidates = findCandidates(element, SearchMode.MAX_2_CANDIDATES);
if (candidates.isEmpty()) return null;
if (candidates.size() == 1) {
return Presentation.of(QuickFixBundle.message("add.method.qualifier.fix.text", candidates.get(0).getName()));
}
if (myCandidates == null) {
myCandidates = findCandidates(SearchMode.MAX_2_CANDIDATES);
}
return !myCandidates.isEmpty();
return Presentation.of(getFamilyName());
}
@NotNull
private List<PsiVariable> findCandidates(@NotNull SearchMode mode) {
PsiMethodCallExpression methodCallElement = myMethodCall.getElement();
if (methodCallElement == null) {
return Collections.emptyList();
}
private List<PsiVariable> findCandidates(@NotNull PsiMethodCallExpression methodCallElement, @NotNull SearchMode mode) {
String methodName = methodCallElement.getMethodExpression().getReferenceName();
if (methodName == null) {
return Collections.emptyList();
@@ -129,70 +76,25 @@ public class AddMethodQualifierFix implements IntentionAction {
return List.copyOf(candidates);
}
@TestOnly
public List<PsiVariable> getCandidates() {
return findCandidates(SearchMode.FULL_SEARCH);
}
@Nullable
@Override
public PsiElement getElementToMakeWritable(@NotNull PsiFile currentFile) {
return myMethodCall.getElement();
protected @NotNull ModCommand perform(@NotNull ActionContext context, @NotNull PsiMethodCallExpression element) {
List<PsiVariable> candidates = findCandidates(element, IntentionPreviewUtils.isIntentionPreviewActive() ?
SearchMode.MAX_2_CANDIDATES : SearchMode.FULL_SEARCH);
SmartPsiElementPointer<PsiMethodCallExpression> pointer = SmartPointerManager.createPointer(element);
List<ModCommandAction> qualifyActions = ContainerUtil.map(candidates, candidate -> createAction(candidate, pointer));
return new ModChooseAction(QuickFixBundle.message("add.qualifier"), qualifyActions);
}
@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
List<PsiVariable> candidates = findCandidates(SearchMode.FULL_SEARCH);
if (candidates.size() == 1 || ApplicationManager.getApplication().isUnitTestMode()) {
qualify(candidates.get(0), editor);
}
else {
chooseAndQualify(project, editor, candidates);
}
}
@Override
public boolean startInWriteAction() {
return false;
}
private void chooseAndQualify(Project project, Editor editor, List<? extends PsiVariable> candidates) {
BaseListPopupStep<PsiVariable> step =
new BaseListPopupStep<>(QuickFixBundle.message("add.qualifier"), candidates) {
@Override
public PopupStep<?> onChosen(PsiVariable selectedValue, boolean finalChoice) {
if (selectedValue != null && finalChoice) {
qualify(selectedValue, editor);
}
return FINAL_CHOICE;
}
@NotNull
@Override
public String getTextFor(PsiVariable value) {
String name = value.getName();
return name == null ? "" : name;
}
@Override
public Icon getIconFor(PsiVariable aValue) {
return aValue.getIcon(0);
}
};
JBPopup popup = JBPopupFactory.getInstance().createListPopup(project, step, (baseRenderer) -> baseRenderer);
popup.showInBestPositionFor(editor);
}
private void qualify(@NotNull PsiVariable qualifier, @NotNull Editor editor) {
WriteCommandAction.runWriteCommandAction(qualifier.getProject(), () -> {
PsiMethodCallExpression element = myMethodCall.getElement();
if (element == null) {
return;
}
PsiElement replacedExpression = replaceWithQualifier(qualifier, element);
editor.getCaretModel().moveToOffset(replacedExpression.getTextOffset() + replacedExpression.getTextLength());
});
@NotNull
private static ModCommandAction createAction(PsiVariable candidate, SmartPsiElementPointer<PsiMethodCallExpression> pointer) {
return ModCommands.psiUpdateStep(
candidate, requireNonNullElse(candidate.getName(), ""), (var, updater) -> {
PsiMethodCallExpression call = updater.getWritable(pointer.getElement());
if (call == null) return;
PsiElement replacedExpression = replaceWithQualifier(var, call);
updater.moveTo(replacedExpression.getTextOffset() + replacedExpression.getTextLength());
}, var -> requireNonNullElse(var.getNameIdentifier(), var).getTextRange())
.withPresentation(p -> p.withIcon(candidate.getIcon(0)));
}
private static PsiElement replaceWithQualifier(@NotNull PsiVariable qualifier, @NotNull PsiMethodCallExpression oldExpression) {

View File

@@ -836,7 +836,7 @@ public final class QuickFixFactoryImpl extends QuickFixFactory {
@NotNull
@Override
public IntentionAction addMethodQualifierFix(@NotNull PsiMethodCallExpression methodCall) {
return new AddMethodQualifierFix(methodCall);
return new AddMethodQualifierFix(methodCall).asIntention();
}
@NotNull

View File

@@ -0,0 +1,26 @@
public class A {
Project p;
MyElement fieldElement;
static MyElement staticElement;
public A() {
MyElement localElement1 = getElement();
localElement1.getProject()<caret>;
MyElement localElement2 = getElement();
}
interface Project {
}
interface MyElement {
Project getProject();
}
}

View File

@@ -0,0 +1,36 @@
import java.lang.Object;
public class A {
Project p;
MyElement fieldElement;
static MyElement staticElement;
public void m(MyElement paramElement) {
Object o = new Object() {
private final MyElement nestedField;
public void targetMethod(MyElement nestedParamElement) {
final MyElement localElement1 = getElement();
localElement1.getProject()<caret>;
MyElement localElement2 = getElement();
}
}
}
interface Project {
}
interface MyElement {
Project getProject();
}
}

View File

@@ -0,0 +1,26 @@
public class A {
Project p;
MyElement fieldElement;
static MyElement staticElement;
public void targetMethod(MyElement paramElement) {
MyElement localElement1 = getElement();
localElement1.getProject();
MyElement localElement2 = getElement();
}
interface Project {
}
interface MyElement {
Project getProject();
}
}

View File

@@ -0,0 +1,15 @@
class X {
public class ObjectValue {
public ObjectValue get(String name) { return null; }
}
public class JsonObjectValue extends ObjectValue {
public int sizeInBytes() { return 0; }
}
{
JsonObjectValue obj = null;
obj.get("href").siz<caret>eInBytes();
}
}

View File

@@ -0,0 +1,26 @@
public class A {
Project p;
MyElement fieldElement;
static MyElement staticElement;
static {
MyElement localElement1 = getElement();
localElement1.getProject()<caret>;
MyElement localElement2 = getElement();
}
interface Project {
}
interface MyElement {
Project getProject();
}
}

View File

@@ -0,0 +1,26 @@
public class A {
Project p;
MyElement fieldElement;
static MyElement staticElement;
public static void targetMethod(MyElement paramElement) {
MyElement localElement1 = getElement();
localElement1.getProject()<caret>;
MyElement localElement2 = getElement();
}
interface Project {
}
interface MyElement {
Project getProject();
}
}

View File

@@ -1,19 +1,15 @@
// 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 com.intellij.codeInsight.daemon.impl.quickfix;
import com.intellij.JavaTestUtil;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInsight.intention.IntentionActionDelegate;
import com.intellij.psi.PsiNamedElement;
import com.intellij.testFramework.fixtures.JavaCodeInsightFixtureTestCase;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.ui.ChooserInterceptor;
import com.intellij.ui.UiInterceptors;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
/**
* @author Dmitry Batkovich
@@ -26,23 +22,23 @@ public class AddMethodQualifierTest extends JavaCodeInsightFixtureTestCase {
}
public void testNonStaticMethod() {
doTest("fieldElement", "staticElement", "localElement1", "paramElement");
doTest("fieldElement", "localElement1", "paramElement", "staticElement");
}
public void testStaticMethod() {
doTest("staticElement", "localElement1", "paramElement");
doTest("localElement1", "paramElement", "staticElement");
}
public void testNestedMethod() {
doTest("fieldElement", "localElement1", "nestedField", "nestedParamElement", "staticElement", "paramElement");
doTest("fieldElement", "localElement1", "nestedField", "nestedParamElement", "paramElement", "staticElement");
}
public void testConstructor() {
doTest("fieldElement", "staticElement", "localElement1");
doTest("fieldElement", "localElement1", "staticElement");
}
public void testStaticInitializer() {
doTest("staticElement", "localElement1");
doTest("localElement1", "staticElement");
}
public void testFix() {
@@ -55,29 +51,15 @@ public class AddMethodQualifierTest extends JavaCodeInsightFixtureTestCase {
}
private void doTestFix() {
myFixture.configureByFile(getTestName(false) + "Before.java");
final AddMethodQualifierFix quickFix = getQuickFix();
assertNotNull(quickFix);
myFixture.checkPreviewAndLaunchAction(quickFix);
myFixture.checkResultByFile(getTestName(false) + "After.java");
myFixture.configureByFile(getTestName(false) + ".java");
IntentionAction action = myFixture.findSingleIntention("Add ");
myFixture.checkPreviewAndLaunchAction(action);
myFixture.checkResultByFile(getTestName(false) + "_after.java");
}
private void doTest(final String... candidatesNames) {
myFixture.configureByFile(getTestName(false) + ".java");
final AddMethodQualifierFix addMethodQualifierFix = getQuickFix();
if (candidatesNames.length == 0) {
assertNull(addMethodQualifierFix);
return;
}
assertNotNull(addMethodQualifierFix);
final Set<String> actualCandidatesNames =
new TreeSet<>(ContainerUtil.map(addMethodQualifierFix.getCandidates(), (Function<PsiNamedElement, String>)psiNamedElement -> {
final String name = psiNamedElement.getName();
assertNotNull(name);
return name;
}));
final Set<String> expectedCandidatesNames = new TreeSet<>(Arrays.asList(candidatesNames));
assertEquals(expectedCandidatesNames, actualCandidatesNames);
UiInterceptors.register(new ChooserInterceptor(List.of(candidatesNames), "localElement1"));
doTestFix();
}
@Nullable