introduce intention to make class sealed: IDEA-243848

GitOrigin-RevId: e69276f3747898731be50b3fc258211924582a48
This commit is contained in:
Roman.Ivanov
2020-07-01 03:56:44 +00:00
committed by intellij-monorepo-bot
parent e565c90d36
commit 8d1e52195e
21 changed files with 422 additions and 1 deletions
@@ -1875,6 +1875,10 @@
<className>com.intellij.codeInsight.intention.impl.CreateFieldFromParameterAction</className>
<category>Java/Declaration</category>
</intentionAction>
<intentionAction>
<className>com.intellij.codeInsight.intention.impl.MakeSealedAction</className>
<category>Java/Declaration</category>
</intentionAction>
<intentionAction>
<className>com.intellij.codeInsight.intention.impl.UnwrapElseBranchAction</className>
<category>Java/Control Flow</category>
@@ -0,0 +1,167 @@
// Copyright 2000-2020 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.impl;
import com.intellij.codeInsight.FileModificationService;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightingFeature;
import com.intellij.codeInsight.intention.BaseElementAtCaretIntentionAction;
import com.intellij.codeInspection.util.IntentionName;
import com.intellij.java.JavaBundle;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.search.searches.ClassInheritorsSearch;
import com.intellij.psi.search.searches.FunctionalExpressionSearch;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.refactoring.util.CommonRefactoringUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.SequentialModalProgressTask;
import com.intellij.util.SequentialTask;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Objects;
import java.util.stream.Stream;
public class MakeSealedAction extends BaseElementAtCaretIntentionAction {
@Override
@NotNull
public String getFamilyName() {
return JavaBundle.message("intention.family.name.make.sealed");
}
@Override
public @IntentionName @NotNull String getText() {
return JavaBundle.message("intention.name.make.sealed");
}
@Override
public boolean isAvailable(@NotNull Project project, Editor editor, @NotNull PsiElement element) {
PsiClass aClass = PsiTreeUtil.getParentOfType(element, PsiClass.class);
if (aClass == null) return false;
return isAvailable(aClass, editor);
}
private static boolean isAvailable(@NotNull PsiClass aClass, Editor editor) {
if (!HighlightingFeature.SEALED_CLASSES.isAvailable(aClass)) return false;
int offset = editor.getCaretModel().getOffset();
PsiElement lBrace = aClass.getLBrace();
if (lBrace == null) return false;
if (offset >= lBrace.getTextRange().getStartOffset()) return false;
if (aClass.hasModifierProperty(PsiModifier.SEALED)) return false;
if (aClass.getImplementsList() == null) return false;
if (aClass.getPermitsList() != null) return false;
if (aClass.getModifierList() == null) return false;
if (aClass.hasModifierProperty(PsiModifier.FINAL)) return false;
return !aClass.hasAnnotation(CommonClassNames.JAVA_LANG_FUNCTIONAL_INTERFACE);
}
@Override
public void invoke(@NotNull Project project, Editor editor, @NotNull PsiElement element) throws IncorrectOperationException {
PsiClass aClass = PsiTreeUtil.getParentOfType(element, PsiClass.class);
if (aClass == null) return;
if (!isAvailable(aClass, editor)) return;
FileModificationService.getInstance().prepareFileForWrite(aClass.getContainingFile());
if (aClass.isInterface()) {
if (FunctionalExpressionSearch.search(aClass).findFirst() != null) {
String message = JavaBundle.message("intention.error.make.sealed.class.is.used.in.functional.expression");
CommonRefactoringUtil.showErrorHint(project, editor, message, getErrorTitle(), null);
return;
}
}
PsiClass[] inheritors = ClassInheritorsSearch.search(aClass, false).toArray(PsiClass.EMPTY_ARRAY);
String[] names = Stream.of(inheritors)
.map(aClass1 -> aClass1.getQualifiedName())
.toArray(String[]::new);
String[] nonNullNames = StreamEx.of(names).nonNull().toArray(String[]::new);
if (nonNullNames.length != names.length) {
String message = JavaBundle.message("intention.error.make.sealed.class.has.anonymous.inheritors");
CommonRefactoringUtil.showErrorHint(project, editor, message, getErrorTitle(), null);
return;
}
setParentModifier(aClass);
if (nonNullNames.length != 0) {
if (shouldCreatePermitsList(inheritors, aClass.getContainingFile())) {
addPermitsClause(project, aClass, nonNullNames);
}
setInheritorsModifiers(project, inheritors);
}
}
public boolean shouldCreatePermitsList(PsiClass[] inheritors, PsiFile parentFile) {
return !Arrays.stream(inheritors).allMatch(psiClass -> psiClass.getContainingFile() == parentFile);
}
public void setParentModifier(PsiClass aClass) {
ApplicationManager.getApplication().runWriteAction(() -> {
PsiModifierList modifierList = Objects.requireNonNull(aClass.getModifierList());
if (modifierList.hasModifierProperty(PsiModifier.NON_SEALED)) {
modifierList.setModifierProperty(PsiModifier.NON_SEALED, false);
}
modifierList.setModifierProperty(PsiModifier.SEALED, true);
});
}
public void setInheritorsModifiers(@NotNull Project project, PsiClass[] inheritors) {
String title = JavaBundle.message("intention.error.make.sealed.class.task.title.set.inheritors.modifiers");
SequentialModalProgressTask task = new SequentialModalProgressTask(project, title, true);
task.setTask(new SequentialTask() {
private int current = 0;
private final int size = inheritors.length;
@Override
public boolean isDone() {
return current >= size;
}
@Override
public boolean iteration() {
task.getIndicator().setFraction(((double)current) / size);
PsiClass inheritor = inheritors[current];
current++;
PsiModifierList modifierList = inheritor.getModifierList();
assert modifierList != null; // ensured by absence of anonymous classes
if (modifierList.hasModifierProperty(PsiModifier.SEALED) ||
modifierList.hasModifierProperty(PsiModifier.NON_SEALED) ||
modifierList.hasModifierProperty(PsiModifier.FINAL)) {
return isDone();
}
ApplicationManager.getApplication().runWriteAction(() -> {
modifierList.setModifierProperty(PsiModifier.NON_SEALED, true);
});
return isDone();
}
});
ProgressManager.getInstance().run(task);
}
public static void addPermitsClause(@NotNull Project project, PsiClass aClass, String[] nonNullNames) {
String permitsClause = StreamEx.of(nonNullNames).sorted().joining(",", "permits ", "");
PsiReferenceList permitsList = createPermitsClause(project, permitsClause);
PsiReferenceList implementsList = Objects.requireNonNull(aClass.getImplementsList());
ApplicationManager.getApplication().runWriteAction(() -> {
aClass.addAfter(permitsList, implementsList);
});
}
@NotNull
private static PsiReferenceList createPermitsClause(@NotNull Project project, String permitsClause) {
PsiFileFactory factory = PsiFileFactory.getInstance(project);
PsiJavaFile javaFile = (PsiJavaFile)factory.createFileFromText(JavaLanguage.INSTANCE, "class __Dummy " + permitsClause + "{}");
PsiClass newClass = javaFile.getClasses()[0];
return Objects.requireNonNull(newClass.getPermitsList());
}
@Override
public boolean startInWriteAction() {
return false;
}
private static String getErrorTitle() {
return JavaBundle.message("intention.error.make.sealed.class.hint.title");
}
}
@@ -0,0 +1,6 @@
<spot>sealed</spot> class Main { }
<spot>non-sealed</spot> class Direct1 extends Main {}
sealed class Direct2 extends Main {}
class NonDirect extends Direct1 {}
@@ -0,0 +1,6 @@
<spot></spot>class Main { }
<spot></spot>class Direct1 extends Main {}
sealed class Direct2 extends Main {}
class NonDirect extends Direct1 {}
@@ -0,0 +1,6 @@
<html>
<body>
This intention makes the class sealed, lists all direct inheritors in permits clause if inheritors are not in the same file and
adds 'non-sealed' modifier if none of 'sealed'/'non-sealed' modifiers were not specified for inheritors.
</body>
</html>
@@ -0,0 +1,10 @@
// "Make sealed" "true"
public sealed class Main { }
sealed class Direct1 extends Main {}
non-sealed class Direct2 extends Main {}
final class Direct3 extends Main {}
non-sealed class Direct4 extends Main {}
class NonDirect extends Direct1 {}
@@ -0,0 +1,3 @@
// "Make sealed" "true"
public sealed class Main { }
@@ -0,0 +1,8 @@
// "Make sealed" "true"
public sealed class Main { }
non-sealed class Direct1 extends Main {}
non-sealed class Direct2 extends Main {}
class NonDirect extends Direct1 {}
@@ -0,0 +1,5 @@
// "Make sealed" "true"
public sealed class Main { }
non-sealed class Direct extends Main {}
@@ -0,0 +1,5 @@
// "Make sealed" "false"
public final class Ma<caret>in { }
class Direct extends Main {}
@@ -0,0 +1,10 @@
// "Make sealed" "true"
public class Ma<caret>in { }
sealed class Direct1 extends Main {}
non-sealed class Direct2 extends Main {}
final class Direct3 extends Main {}
class Direct4 extends Main {}
class NonDirect extends Direct1 {}
@@ -0,0 +1,3 @@
// "Make sealed" "true"
public class Ma<caret>in { }
@@ -0,0 +1,8 @@
// "Make sealed" "true"
public class Ma<caret>in { }
class Direct1 extends Main {}
class Direct2 extends Main {}
class NonDirect extends Direct1 {}
@@ -0,0 +1,5 @@
// "Make sealed" "true"
public non-sealed class Ma<caret>in { }
class Direct extends Main {}
@@ -0,0 +1,5 @@
// "Make sealed" "false"
public sealed class Ma<caret>in { }
class Direct extends Main {}
@@ -0,0 +1,21 @@
interface Funct<caret>ional {
void run();
}
class Inheritor implements Functional {
@Override
public void run() {
}
}
class A {
void foo() {
Functional f = new Functional() {
@Override
public void run() {
}
};
}
}
@@ -0,0 +1,9 @@
interface Fun<caret>ctional {
void run();
}
class A {
void foo() {
Functional f = () -> {};
}
}
@@ -0,0 +1,53 @@
/*
* 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.java.codeInsight.intention;
import com.intellij.codeInsight.intention.impl.MakeSealedAction;
import com.intellij.java.JavaBundle;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.refactoring.util.CommonRefactoringUtil;
import com.intellij.testFramework.LightJavaCodeInsightTestCase;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import org.jetbrains.annotations.NotNull;
public class MakeSealedActionFailingTest extends LightJavaCodeInsightTestCase {
@Override
protected @NotNull LightProjectDescriptor getProjectDescriptor() {
return LightJavaCodeInsightFixtureTestCase.JAVA_15;
}
public void testFunctionalInterface() {
checkErrorMessage(JavaBundle.message("intention.error.make.sealed.class.is.used.in.functional.expression"));
}
public void testAnonymousClass() {
checkErrorMessage(JavaBundle.message("intention.error.make.sealed.class.has.anonymous.inheritors"));
}
private void checkErrorMessage(@NotNull String message) {
configureByFile("/codeInsight/daemonCodeAnalyzer/quickFix/makeClassSealed/failing/" + getTestName(false) + ".java");
MakeSealedAction action = new MakeSealedAction();
assertTrue(action.isAvailable(getProject(), getEditor(), getFile()));
try {
ApplicationManager.getApplication().runWriteAction(() -> action.invoke(getProject(), getEditor(), getFile()));
} catch (CommonRefactoringUtil.RefactoringErrorHintException e) {
assertEquals(message, e.getMessage());
return;
}
fail("Test must fail with error message");
}
}
@@ -0,0 +1,48 @@
/*
* 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.java.codeInsight.intention;
import com.intellij.codeInsight.intention.impl.MakeSealedAction;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiFile;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import org.jetbrains.annotations.NotNull;
public class MakeSealedActionMultiFileTest extends LightJavaCodeInsightFixtureTestCase {
@Override
protected @NotNull LightProjectDescriptor getProjectDescriptor() {
return LightJavaCodeInsightFixtureTestCase.JAVA_15;
}
public void testMultiFile() {
PsiFile main = myFixture.configureByText("Main.java", "class Mai<caret>n {}");
PsiClass direct1 = myFixture.addClass("class Direct1 extends Main {}");
PsiClass direct2 = myFixture.addClass("class Direct2 extends Main {}");
PsiClass indirect = myFixture.addClass("class Indirect extends Direct1 {}");
MakeSealedAction action = new MakeSealedAction();
assertTrue(action.isAvailable(getProject(), getEditor(), getFile()));
WriteCommandAction.runWriteCommandAction(getProject(), () -> {
action.invoke(getProject(), getEditor(), getFile());
});
assertEquals("sealed class Main permits Direct1, Direct2 {}", main.getText());
assertEquals("non-sealed class Direct1 extends Main {}", direct1.getText());
assertEquals("non-sealed class Direct2 extends Main {}", direct2.getText());
assertEquals("class Indirect extends Direct1 {}", indirect.getText());
}
}
@@ -0,0 +1,33 @@
/*
* 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.java.codeInsight.intention;
import com.intellij.codeInsight.daemon.LightIntentionActionTestCase;
import com.intellij.testFramework.LightProjectDescriptor;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import org.jetbrains.annotations.NotNull;
public class MakeSealedActionTest extends LightIntentionActionTestCase {
@Override
protected @NotNull LightProjectDescriptor getProjectDescriptor() {
return LightJavaCodeInsightFixtureTestCase.JAVA_15;
}
@Override
protected String getBasePath() {
return "/codeInsight/daemonCodeAnalyzer/quickFix/makeClassSealed";
}
}
@@ -1259,4 +1259,10 @@ slice.usage.message.in.file.stopped.here=(in {0} file - stopped here)
slice.usage.message.tracking.container.contents=(Tracking container ''{0}{1}'' contents)
slice.usage.message.location=in {0}
intention.name.move.into.if.branches=Move up into 'if' statement branches
intention.name.collapse.into.loop=Collapse into loop
intention.name.collapse.into.loop=Collapse into loop
intention.family.name.make.sealed=Make class sealed
intention.name.make.sealed=Make sealed
intention.error.make.sealed.class.is.used.in.functional.expression=Class is used in functional expression
intention.error.make.sealed.class.hint.title=Make Sealed
intention.error.make.sealed.class.has.anonymous.inheritors=Some of the inheritors are anonymous
intention.error.make.sealed.class.task.title.set.inheritors.modifiers=Setting inheritors modifiers