Java: Quick fix for merging duplicate statements in module-info (IDEA-169211)

This commit is contained in:
Pavel Dolgov
2017-03-30 18:17:34 +03:00
parent 9d74ed70a6
commit 79bbbbf8ff
20 changed files with 488 additions and 0 deletions

View File

@@ -197,6 +197,7 @@ public class ModuleHighlightUtil {
String message = JavaErrorMessages.message(key, refText);
HighlightInfo info = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR).range(statement).descriptionAndTooltip(message).create();
QuickFixAction.registerQuickFixAction(info, factory().createDeleteFix(statement));
QuickFixAction.registerQuickFixAction(info, MergeModuleStatementsFix.createFix(statement));
results.add(info);
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2000-2017 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.
*/
package com.intellij.codeInsight.daemon.impl.quickfix;
import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.util.PsiUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Iterator;
import java.util.List;
import java.util.StringJoiner;
/**
* @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);
}
@Override
public boolean isAvailable(@NotNull Project project,
@NotNull PsiFile file,
@NotNull PsiElement startElement,
@NotNull PsiElement endElement) {
final T otherStatement = myOtherStatement.getElement();
return otherStatement != null && otherStatement.isValid() && PsiUtil.isLanguageLevel9OrHigher(file);
}
@Override
public void invoke(@NotNull Project project,
@NotNull PsiFile file,
@Nullable Editor editor,
@NotNull PsiElement thisStatement,
@NotNull PsiElement endElement) {
final T otherStatement = myOtherStatement.getElement();
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 Iterator<T> statementIterator = getStatements(tempModule).iterator();
LOG.assertTrue(statementIterator.hasNext());
final T replacement = statementIterator.next();
final CodeStyleManager codeStyleManager = CodeStyleManager.getInstance(project);
codeStyleManager.reformat(otherStatement.replace(replacement));
thisStatement.delete();
}
}
}
@NotNull
protected abstract String getReplacementText(@NotNull T otherStatement);
@NotNull
protected abstract Iterable<T> getStatements(@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();
}
@Nullable
public static MergeModuleStatementsFix createFix(@Nullable PsiElement statement) {
if (statement instanceof PsiPackageAccessibilityStatement) {
return MergePackageAccessibilityStatementsFix.createFix((PsiPackageAccessibilityStatement)statement);
}
else if (statement instanceof PsiProvidesStatement) {
return MergeProvidesStatementsFix.createFix((PsiProvidesStatement)statement);
}
return null;
}
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright 2000-2017 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.
*/
package com.intellij.codeInsight.daemon.impl.quickfix;
import com.intellij.codeInsight.daemon.QuickFixBundle;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiJavaModule;
import com.intellij.psi.PsiKeyword;
import com.intellij.psi.PsiPackageAccessibilityStatement;
import com.intellij.psi.PsiPackageAccessibilityStatement.Role;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
/**
* @author Pavel.Dolgov
*/
public class MergePackageAccessibilityStatementsFix
extends MergeModuleStatementsFix<PsiPackageAccessibilityStatement> {
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);
myPackageName = packageName;
myModuleNames = moduleNames;
myRole = thisStatement.getRole();
}
@Nls
@NotNull
@Override
public String getText() {
return QuickFixBundle.message("java.9.merge.module.statements.fix.name", getKeyword(), myPackageName);
}
@Nls
@NotNull
@Override
public String getFamilyName() {
return QuickFixBundle.message("java.9.merge.module.statements.fix.family.name", getKeyword());
}
@NotNull
@Override
protected String getReplacementText(@NotNull PsiPackageAccessibilityStatement otherStatement) {
return getKeyword() + " " + myPackageName + " " + PsiKeyword.TO + " " +
joinNames(otherStatement.getModuleNames(), myModuleNames) + ";";
}
@NotNull
@Override
protected Iterable<PsiPackageAccessibilityStatement> getStatements(@NotNull PsiJavaModule javaModule) {
return getStatements(javaModule, myRole);
}
@Nullable
public static MergeModuleStatementsFix createFix(@Nullable PsiPackageAccessibilityStatement statement) {
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()) {
for (PsiPackageAccessibilityStatement candidate : getStatements(javaModule, statement.getRole())) {
if (candidate != statement &&
packageName.equals(candidate.getPackageName()) &&
candidate.getModuleNames().iterator().hasNext()) {
return new MergePackageAccessibilityStatementsFix(statement, packageName, moduleNames, candidate);
}
}
}
}
}
}
return null;
}
@NotNull
private static Iterable<PsiPackageAccessibilityStatement> getStatements(@NotNull PsiJavaModule javaModule, @NotNull Role role) {
switch (role) {
case OPENS:
return javaModule.getOpens();
case EXPORTS:
return javaModule.getExports();
}
LOG.error("Unexpected role " + role);
return Collections.emptyList();
}
@NotNull
private String getKeyword() {
switch (myRole) {
case OPENS:
return PsiKeyword.OPENS;
case EXPORTS:
return PsiKeyword.EXPORTS;
}
LOG.error("Unexpected role " + myRole);
return "";
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2000-2017 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.
*/
package com.intellij.codeInsight.daemon.impl.quickfix;
import com.intellij.codeInsight.daemon.QuickFixBundle;
import com.intellij.psi.*;
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);
myInterfaceName = interfaceName;
myImplementationNames = implementationNames;
}
@NotNull
@Override
public String getText() {
return QuickFixBundle.message("java.9.merge.module.statements.fix.name", PsiKeyword.PROVIDES, myInterfaceName);
}
@Nls
@NotNull
@Override
public String getFamilyName() {
return QuickFixBundle.message("java.9.merge.module.statements.fix.family.name", PsiKeyword.PROVIDES);
}
@NotNull
@Override
protected String getReplacementText(@NotNull PsiProvidesStatement otherStatement) {
return PsiKeyword.PROVIDES + " " + myInterfaceName + " " + PsiKeyword.WITH + " " +
joinNames(getImplementationNames(otherStatement), myImplementationNames) + ";";
}
@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();
}
@Nullable
public static MergeModuleStatementsFix createFix(@Nullable PsiProvidesStatement statement) {
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 null;
}
}

View File

@@ -0,0 +1,4 @@
module M {
exports my.api to M4;
exports <caret>my.api to M6;
}

View File

@@ -0,0 +1,3 @@
module M {
exports my.api to M4, M6;
}

View File

@@ -0,0 +1,5 @@
module M {
exports my.api;
exports <caret>my.api to M2, M4;
exports my.api to M6;
}

View File

@@ -0,0 +1,4 @@
module M {
exports my.api;
exports my.api to M6, M2, M4;
}

View File

@@ -0,0 +1,4 @@
module M {
opens my.api to M4;
opens <caret>my.api to M6;
}

View File

@@ -0,0 +1,3 @@
module M {
opens my.api to M4, M6;
}

View File

@@ -0,0 +1,5 @@
module M {
opens my.api;
opens <caret>my.api to M2, M4;
opens my.api to M6;
}

View File

@@ -0,0 +1,4 @@
module M {
opens my.api;
opens my.api to M6, M2, M4;
}

View File

@@ -0,0 +1,4 @@
module M {
provides my.api.MyService with my.impl.MyServiceImpl;
provides my.api.MyService with my.impl.<caret>MyServiceImpl1;
}

View File

@@ -0,0 +1,3 @@
module M {
provides my.api.MyService with my.impl.MyServiceImpl,my.impl.MyServiceImpl1;
}

View File

@@ -0,0 +1,8 @@
import my.impl.MyServiceImpl;
import my.impl.MyServiceImpl1;
import my.impl.MyServiceImpl2;
module M {
provides my.api.MyService with MyServiceImpl, MyServiceImpl2;
provides my.api.MyService with <caret>MyServiceImpl1;
}

View File

@@ -0,0 +1,7 @@
import my.impl.MyServiceImpl;
import my.impl.MyServiceImpl1;
import my.impl.MyServiceImpl2;
module M {
provides my.api.MyService with MyServiceImpl,MyServiceImpl2,MyServiceImpl1;
}

View File

@@ -0,0 +1,8 @@
import my.impl.MyServiceImpl;
import my.impl.MyServiceImpl1;
import my.impl.MyServiceImpl2;
module M {
provides my.api.MyService with MyServiceImpl;
provides my.api.MyService with <caret>MyServiceImpl1, MyServiceImpl2;
}

View File

@@ -0,0 +1,7 @@
import my.impl.MyServiceImpl;
import my.impl.MyServiceImpl1;
import my.impl.MyServiceImpl2;
module M {
provides my.api.MyService with MyServiceImpl,MyServiceImpl1,MyServiceImpl2;
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2000-2017 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.
*/
package com.intellij.codeInsight.daemon.quickFix
import com.intellij.JavaTestUtil.getRelativeJavaTestDataPath
import com.intellij.codeInsight.daemon.QuickFixBundle
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.testFramework.fixtures.LightJava9ModulesCodeInsightFixtureTestCase
import com.intellij.testFramework.fixtures.MultiModuleJava9ProjectDescriptor.ModuleDescriptor.*
/**
* @author Pavel.Dolgov
*/
class MergeModuleStatementsFixTest : LightJava9ModulesCodeInsightFixtureTestCase() {
override fun getBasePath() = getRelativeJavaTestDataPath() + "/codeInsight/daemonCodeAnalyzer/quickFix/mergeModuleStatementsFix"
fun testExports1() = doTest("exports", "my.api")
fun testExports2() = 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")
override fun setUp() {
super.setUp()
addFile("module-info.java", "module M2 { }", M2)
addFile("module-info.java", "module M4 { }", M4)
addFile("module-info.java", "module M6 { }", M6)
addFile("my/api/MyService.java", "package my.api; public class MyService {}")
addFile("my/impl/MyServiceImpl.java", "package my.impl; public class MyServiceImpl extends my.api.MyService {}")
addFile("my/impl/MyServiceImpl1.java", "package my.impl; public class MyServiceImpl1 extends my.api.MyService {}")
addFile("my/impl/MyServiceImpl2.java", "package my.impl; public class MyServiceImpl2 extends my.api.MyService {}")
}
private fun doTest(type: String, name: String) {
val testName = getTestName(false)
val virtualFile = myFixture.copyFileToProject("${testName}.java", "module-info.java")
myFixture.configureFromExistingVirtualFile(virtualFile)
val action = findActionWithText(QuickFixBundle.message("java.9.merge.module.statements.fix.name", type, name))
myFixture.launchAction(action)
myFixture.checkResultByFile("${testName}_after.java")
}
private fun findActionWithText(actionText: String): IntentionAction {
myFixture.doHighlighting()
val actions = LightQuickFixTestCase.getAvailableActions(editor, file)
val action = LightQuickFixTestCase.findActionWithText(actions, actionText)
assertNotNull("No action [$actionText] in ${actions.map { it.text }}", action)
return action
}
}

View File

@@ -310,3 +310,6 @@ insert.sam.method.call.fix.family.name=Insert single abstract method call
wrap.with.java.io.file.text=Wrap using 'new File()'
wrap.with.java.io.file.parameter.single.text=Wrap parameter using 'new File()'
wrap.with.java.io.file.parameter.multiple.text=Wrap {0, choice, 1#1st|2#2nd|3#3rd|4#{0,number}th} parameter using ''new File()''
java.9.merge.module.statements.fix.family.name=Merge with other ''{0}'' statement
java.9.merge.module.statements.fix.name=Merge with other ''{0} {1}'' statement