mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
introduce intention to make class sealed: IDEA-243848
GitOrigin-RevId: e69276f3747898731be50b3fc258211924582a48
This commit is contained in:
committed by
intellij-monorepo-bot
parent
e565c90d36
commit
8d1e52195e
@@ -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>
|
||||
+10
@@ -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 {}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
// "Make sealed" "true"
|
||||
|
||||
public sealed class Main { }
|
||||
+8
@@ -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 {}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// "Make sealed" "true"
|
||||
|
||||
public sealed class Main { }
|
||||
|
||||
non-sealed class Direct extends Main {}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// "Make sealed" "false"
|
||||
|
||||
public final class Ma<caret>in { }
|
||||
|
||||
class Direct extends Main {}
|
||||
+10
@@ -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 {}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
// "Make sealed" "true"
|
||||
|
||||
public class Ma<caret>in { }
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// "Make sealed" "true"
|
||||
|
||||
public class Ma<caret>in { }
|
||||
|
||||
class Direct1 extends Main {}
|
||||
class Direct2 extends Main {}
|
||||
|
||||
class NonDirect extends Direct1 {}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// "Make sealed" "true"
|
||||
|
||||
public non-sealed class Ma<caret>in { }
|
||||
|
||||
class Direct extends Main {}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// "Make sealed" "false"
|
||||
|
||||
public sealed class Ma<caret>in { }
|
||||
|
||||
class Direct extends Main {}
|
||||
+21
@@ -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() {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
interface Fun<caret>ctional {
|
||||
void run();
|
||||
}
|
||||
|
||||
class A {
|
||||
void foo() {
|
||||
Functional f = () -> {};
|
||||
}
|
||||
}
|
||||
+53
@@ -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");
|
||||
}
|
||||
}
|
||||
+48
@@ -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());
|
||||
}
|
||||
}
|
||||
+33
@@ -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
|
||||
Reference in New Issue
Block a user