Merge duplicate statements in module-info.java - merge all duplicates at once, preserve comments, set caret to the resulting statement (IDEA-169211)

This commit is contained in:
Pavel Dolgov
2017-04-19 13:57:14 +03:00
parent 874a7b88cd
commit af08eeeeb9
16 changed files with 136 additions and 113 deletions
@@ -21,23 +21,22 @@ import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.util.PsiUtil;
import com.siyeh.ig.psiutils.CommentTracker;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Iterator;
import java.util.List;
import java.util.StringJoiner;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author Pavel.Dolgov
*/
public abstract class MergeModuleStatementsFix<T extends PsiElement> extends LocalQuickFixAndIntentionActionOnPsiElement {
protected final SmartPsiElementPointer<T> myOtherStatement;
protected MergeModuleStatementsFix(@NotNull T thisStatement, @NotNull T otherStatement) {
super(thisStatement);
final PsiFile file = otherStatement.getContainingFile();
myOtherStatement = SmartPointerManager.getInstance(otherStatement.getProject()).createSmartPsiElementPointer(otherStatement, file);
protected MergeModuleStatementsFix(@NotNull PsiJavaModule javaModule) {
super(javaModule);
}
@Override
@@ -45,49 +44,57 @@ public abstract class MergeModuleStatementsFix<T extends PsiElement> extends Loc
@NotNull PsiFile file,
@NotNull PsiElement startElement,
@NotNull PsiElement endElement) {
final T otherStatement = myOtherStatement.getElement();
return otherStatement != null && otherStatement.isValid() && PsiUtil.isLanguageLevel9OrHigher(file);
return PsiUtil.isLanguageLevel9OrHigher(file);
}
@Override
public void invoke(@NotNull Project project,
@NotNull PsiFile file,
@Nullable Editor editor,
@NotNull PsiElement thisStatement,
@NotNull PsiElement startElement,
@NotNull PsiElement endElement) {
final T otherStatement = myOtherStatement.getElement();
if (startElement instanceof PsiJavaModule) {
final PsiJavaModule javaModule = (PsiJavaModule)startElement;
final List<T> statementsToMerge = getStatementsToMerge(javaModule);
LOG.assertTrue(!statementsToMerge.isEmpty());
if (otherStatement != null) {
final PsiElement parent = otherStatement.getParent();
if (parent instanceof PsiJavaModule) {
final String moduleName = ((PsiJavaModule)parent).getName();
final String moduleText = PsiKeyword.MODULE + " " + moduleName + " {" + getReplacementText(otherStatement) + "}";
final PsiElementFactory factory = JavaPsiFacade.getInstance(project).getElementFactory();
final PsiJavaModule tempModule = factory.createModuleFromText(moduleText);
final String tempModuleText = PsiKeyword.MODULE + " " + javaModule.getName() + " {" + getReplacementText(statementsToMerge) + "}";
final PsiJavaModule tempModule = JavaPsiFacade.getInstance(project).getElementFactory().createModuleFromText(tempModuleText);
final Iterator<T> statementIterator = getStatements(tempModule).iterator();
LOG.assertTrue(statementIterator.hasNext());
final T replacement = statementIterator.next();
final List<T> tempStatements = getStatementsToMerge(tempModule);
LOG.assertTrue(!tempStatements.isEmpty());
final T replacement = tempStatements.get(0);
final CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(project);
codeStyleManager.reformat(otherStatement.replace(replacement));
thisStatement.delete();
final T firstStatement = statementsToMerge.get(0);
final CommentTracker commentTracker = new CommentTracker();
final CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(project);
final PsiElement resultingStatement = codeStyleManager.reformat(commentTracker.replace(firstStatement, replacement));
for (int i = 1; i < statementsToMerge.size(); i++) {
T statement = statementsToMerge.get(i);
commentTracker.delete(statement);
}
commentTracker.insertCommentsBefore(resultingStatement);
if (editor != null) {
final int offset = resultingStatement.getTextRange().getEndOffset();
editor.getCaretModel().moveToOffset(offset);
}
}
}
@NotNull
protected abstract String getReplacementText(@NotNull T otherStatement);
protected abstract String getReplacementText(List<T> statementsToMerge);
@NotNull
protected abstract Iterable<T> getStatements(@NotNull PsiJavaModule javaModule);
protected abstract List<T> getStatementsToMerge(@NotNull PsiJavaModule javaModule);
@NotNull
protected static String joinNames(@NotNull List<String> oldNames, @NotNull List<String> newNames) {
final StringJoiner joiner = new StringJoiner(",");
oldNames.forEach(joiner::add);
newNames.stream().filter(name -> !oldNames.contains(name)).forEach(joiner::add);
return joiner.toString();
protected static String joinUniqueNames(@NotNull List<String> names) {
final Set<String> unique = new THashSet<>();
return names.stream()
.filter(name -> unique.add(name))
.collect(Collectors.joining(","));
}
@Nullable
@@ -22,10 +22,12 @@ import com.intellij.psi.PsiJavaModule;
import com.intellij.psi.PsiKeyword;
import com.intellij.psi.PsiPackageAccessibilityStatement;
import com.intellij.psi.PsiPackageAccessibilityStatement.Role;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -37,17 +39,12 @@ public class MergePackageAccessibilityStatementsFix
private static final Logger LOG = Logger.getInstance(MergePackageAccessibilityStatementsFix.class);
private final String myPackageName;
private final List<String> myModuleNames;
private final Role myRole;
protected MergePackageAccessibilityStatementsFix(@NotNull PsiPackageAccessibilityStatement thisStatement,
@NotNull String packageName,
@NotNull List<String> moduleNames,
@NotNull PsiPackageAccessibilityStatement otherStatement) {
super(thisStatement, otherStatement);
protected MergePackageAccessibilityStatementsFix(@NotNull PsiJavaModule javaModule, @NotNull String packageName, @NotNull Role role) {
super(javaModule);
myPackageName = packageName;
myModuleNames = moduleNames;
myRole = thisStatement.getRole();
myRole = role;
}
@Nls
@@ -66,15 +63,33 @@ public class MergePackageAccessibilityStatementsFix
@NotNull
@Override
protected String getReplacementText(@NotNull PsiPackageAccessibilityStatement otherStatement) {
return getKeyword() + " " + myPackageName + " " + PsiKeyword.TO + " " +
joinNames(otherStatement.getModuleNames(), myModuleNames) + ";";
protected String getReplacementText(@NotNull List<PsiPackageAccessibilityStatement> statementsToMerge) {
final List<String> moduleNames = getModuleNames(statementsToMerge);
if (!moduleNames.isEmpty()) {
return getKeyword() + " " + myPackageName + " " + PsiKeyword.TO + " " + joinUniqueNames(moduleNames) + ";";
}
return getKeyword() + " " + myPackageName + ";";
}
@NotNull
private static List<String> getModuleNames(@NotNull List<PsiPackageAccessibilityStatement> statements) {
final List<String> result = new ArrayList<>();
for (PsiPackageAccessibilityStatement statement : statements) {
final List<String> moduleNames = statement.getModuleNames();
if (moduleNames.isEmpty()) {
return Collections.emptyList();
}
result.addAll(moduleNames);
}
return result;
}
@NotNull
@Override
protected Iterable<PsiPackageAccessibilityStatement> getStatements(@NotNull PsiJavaModule javaModule) {
return getStatements(javaModule, myRole);
protected List<PsiPackageAccessibilityStatement> getStatementsToMerge(@NotNull PsiJavaModule javaModule) {
return StreamEx.of(getStatements(javaModule, myRole).iterator())
.filter(statement -> myPackageName.equals(statement.getPackageName()))
.toList();
}
@Nullable
@@ -82,28 +97,9 @@ public class MergePackageAccessibilityStatementsFix
if (statement != null) {
final PsiElement parent = statement.getParent();
if (parent instanceof PsiJavaModule) {
final PsiJavaModule javaModule = (PsiJavaModule)parent;
final String packageName = statement.getPackageName();
if (packageName != null) {
final List<String> moduleNames = statement.getModuleNames();
if (!moduleNames.isEmpty()) {
PsiPackageAccessibilityStatement targetStatement = null;
for (PsiPackageAccessibilityStatement candidate : getStatements(javaModule, statement.getRole())) {
if (candidate != statement && packageName.equals(candidate.getPackageName())) {
if (candidate.getModuleNames().isEmpty()) {
// merging with a statement that has no target modules is equivalent to deletion; deletion is a different fix
return null;
}
if (targetStatement == null) {
targetStatement = candidate;
}
}
}
if (targetStatement != null) {
return new MergePackageAccessibilityStatementsFix(statement, packageName, moduleNames, targetStatement);
}
}
return new MergePackageAccessibilityStatementsFix((PsiJavaModule)parent, packageName, statement.getRole());
}
}
}
@@ -17,30 +17,23 @@ package com.intellij.codeInsight.daemon.impl.quickfix;
import com.intellij.codeInsight.daemon.QuickFixBundle;
import com.intellij.psi.*;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* @author Pavel.Dolgov
*/
public class MergeProvidesStatementsFix extends MergeModuleStatementsFix<PsiProvidesStatement> {
private final String myInterfaceName;
private final List<String> myImplementationNames;
MergeProvidesStatementsFix(@NotNull PsiProvidesStatement thisStatement,
@NotNull String interfaceName,
@NotNull List<String> implementationNames,
@NotNull PsiProvidesStatement otherStatement) {
super(thisStatement, otherStatement);
MergeProvidesStatementsFix(@NotNull PsiJavaModule javaModule, @NotNull String interfaceName) {
super(javaModule);
myInterfaceName = interfaceName;
myImplementationNames = implementationNames;
}
@NotNull
@@ -58,29 +51,33 @@ public class MergeProvidesStatementsFix extends MergeModuleStatementsFix<PsiProv
@NotNull
@Override
protected String getReplacementText(@NotNull PsiProvidesStatement otherStatement) {
return PsiKeyword.PROVIDES + " " + myInterfaceName + " " + PsiKeyword.WITH + " " +
joinNames(getImplementationNames(otherStatement), myImplementationNames) + ";";
protected String getReplacementText(@NotNull List<PsiProvidesStatement> statementsToMerge) {
final List<String> implementationNames = getImplementationNames(statementsToMerge);
LOG.assertTrue(!implementationNames.isEmpty());
return PsiKeyword.PROVIDES + " " + myInterfaceName + " " + PsiKeyword.WITH + " " + joinUniqueNames(implementationNames) + ";";
}
@NotNull
private static List<String> getImplementationNames(@NotNull List<PsiProvidesStatement> statements) {
return StreamEx.of(statements)
.map(PsiProvidesStatement::getImplementationList)
.nonNull()
.flatMap(implementationList -> Arrays.stream(implementationList.getReferenceElements()))
.nonNull()
.map(PsiJavaCodeReferenceElement::getQualifiedName)
.nonNull()
.toList();
}
@NotNull
@Override
protected Iterable<PsiProvidesStatement> getStatements(@NotNull PsiJavaModule javaModule) {
return javaModule.getProvides();
}
@NotNull
private static List<String> getImplementationNames(@Nullable PsiProvidesStatement statement) {
if (statement != null) {
final PsiReferenceList implementationList = statement.getImplementationList();
if (implementationList != null) {
return Arrays.stream(implementationList.getReferenceElements())
.map(PsiJavaCodeReferenceElement::getQualifiedName)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
}
return Collections.emptyList();
protected List<PsiProvidesStatement> getStatementsToMerge(@NotNull PsiJavaModule javaModule) {
return StreamEx.of(javaModule.getProvides().iterator())
.filter(statement -> {
final PsiJavaCodeReferenceElement reference = statement.getInterfaceReference();
return reference != null && myInterfaceName.equals(reference.getQualifiedName());
})
.toList();
}
@Nullable
@@ -88,21 +85,11 @@ public class MergeProvidesStatementsFix extends MergeModuleStatementsFix<PsiProv
if (statement != null) {
final PsiElement parent = statement.getParent();
if (parent instanceof PsiJavaModule) {
final PsiJavaModule javaModule = (PsiJavaModule)parent;
final PsiJavaCodeReferenceElement interfaceReference = statement.getInterfaceReference();
if (interfaceReference != null) {
final String interfaceName = interfaceReference.getQualifiedName();
if (interfaceName != null) {
final List<String> implementationNames = getImplementationNames(statement);
if (!implementationNames.isEmpty()) {
for (PsiProvidesStatement candidate : javaModule.getProvides()) {
final PsiJavaCodeReferenceElement candidateInterfaceReference = candidate.getInterfaceReference();
if (candidateInterfaceReference != null && interfaceName.equals(candidateInterfaceReference.getQualifiedName())) {
return new MergeProvidesStatementsFix(statement, interfaceName, implementationNames, candidate);
}
}
}
return new MergeProvidesStatementsFix((PsiJavaModule)parent, interfaceName);
}
}
}
@@ -1,3 +1,3 @@
module M {
exports my.api to M4, M6;
exports my.api to M4, M6;<caret>
}
@@ -1,5 +1,5 @@
module M {
exports <caret>my.api to M2, M4;
exports my.api to M6;
exports my.api to M2, M4;
exports <caret>my.api to M6;
exports my.api;
}
@@ -0,0 +1,3 @@
module M {
exports my.api;<caret>
}
@@ -0,0 +1,8 @@
module M {
// first
exports my.api to M2;
// second
exports <caret>my.api to M6;
// third
exports my.api to M4;
}
@@ -0,0 +1,6 @@
module M {
// first
exports my.api to M2, M6, M4;<caret>
// second
// third
}
@@ -1,3 +1,3 @@
module M {
opens my.api to M4, M6;
opens my.api to M4, M6;<caret>
}
@@ -0,0 +1,3 @@
module M {
opens my.api;<caret>
}
@@ -0,0 +1,5 @@
module M {
opens /*first*/ my.api to M4;
opens /*second*/ <caret>my.api to M2;
opens /*third*/ my.api to M6;
}
@@ -0,0 +1,6 @@
module M {
/*first*/
/*second*/
/*third*/
opens my.api to M4, M2, M6;<caret>
}
@@ -1,3 +1,3 @@
module M {
provides my.api.MyService with my.impl.MyServiceImpl,my.impl.MyServiceImpl1;
provides my.api.MyService with my.impl.MyServiceImpl,my.impl.MyServiceImpl1;<caret>
}
@@ -3,5 +3,5 @@ import my.impl.MyServiceImpl1;
import my.impl.MyServiceImpl2;
module M {
provides my.api.MyService with MyServiceImpl,MyServiceImpl2,MyServiceImpl1;
provides my.api.MyService with MyServiceImpl,MyServiceImpl2,MyServiceImpl1;<caret>
}
@@ -3,5 +3,5 @@ import my.impl.MyServiceImpl1;
import my.impl.MyServiceImpl2;
module M {
provides my.api.MyService with MyServiceImpl,MyServiceImpl1,MyServiceImpl2;
provides my.api.MyService with MyServiceImpl,MyServiceImpl1,MyServiceImpl2;<caret>
}
@@ -28,14 +28,16 @@ class MergeModuleStatementsFixTest : LightJava9ModulesCodeInsightFixtureTestCase
override fun getBasePath() = getRelativeJavaTestDataPath() + "/codeInsight/daemonCodeAnalyzer/quickFix/mergeModuleStatementsFix"
fun testExports1() = doTest("exports", "my.api")
fun testExports2() = doTest("exports", "my.api", false)
fun testExports2() = doTest("exports", "my.api")
fun testExports3() = doTest("exports", "my.api")
fun testProvides1() = doTest("provides", "my.api.MyService")
fun testProvides2() = doTest("provides", "my.api.MyService")
fun testProvides3() = doTest("provides", "my.api.MyService")
fun testOpens1() = doTest("opens", "my.api")
fun testOpens2() = doTest("opens", "my.api", false)
fun testOpens2() = doTest("opens", "my.api")
fun testOpens3() = doTest("opens", "my.api")
override fun setUp() {