MemberModel: detect possible out of class definitions and suggest to move them into class (IDEA-258839)

GitOrigin-RevId: 7708fdc7b3191bcd0ba46767160c4ec0b5a179da
This commit is contained in:
Artemiy Sartakov
2021-03-01 14:54:40 +07:00
committed by intellij-monorepo-bot
parent 49252c34ab
commit ccc5077467
36 changed files with 735 additions and 0 deletions

View File

@@ -501,4 +501,6 @@ public abstract class QuickFixFactory {
public abstract @NotNull IntentionAction createSealClassFromPermitsListFix(@NotNull PsiClass classFromPermitsList);
public abstract @NotNull IntentionAction createUnimplementInterfaceAction(PsiJavaCodeReferenceElement ref, boolean isDuplicates);
public abstract @NotNull IntentionAction createMoveMemberIntoClassFix(@NotNull PsiErrorElement errorElement);
}

View File

@@ -281,6 +281,17 @@ public final class HighlightClassUtil {
QuickFixAction.registerQuickFixAction(errorResult, QUICK_FIX_FACTORY.createRenameElementFix(aClass));
return errorResult;
}
static HighlightInfo checkClassMemberDeclaredOutside(@NotNull PsiErrorElement errorElement) {
MemberModel model = MemberModel.create(errorElement);
if (model == null) return null;
HighlightInfo info = HighlightInfo.newHighlightInfo(HighlightInfoType.ERROR)
.range(model.textRange())
.description(JavaErrorBundle.message("class.member.declared.outside"))
.create();
QuickFixAction.registerQuickFixAction(info, QUICK_FIX_FACTORY.createMoveMemberIntoClassFix(errorElement));
return info;
}
/**
* @return true if file correspond to the shebang script

View File

@@ -924,6 +924,12 @@ public class HighlightVisitorImpl extends JavaElementVisitor implements Highligh
}
}
@Override
public void visitErrorElement(@NotNull PsiErrorElement element) {
super.visitErrorElement(element);
myHolder.add(HighlightClassUtil.checkClassMemberDeclaredOutside(element));
}
@Override
public void visitMethod(PsiMethod method) {
super.visitMethod(method);

View File

@@ -0,0 +1,281 @@
// Copyright 2000-2021 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.daemon.impl.analysis;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.ElementType;
import com.intellij.psi.tree.IElementType;
import com.intellij.util.containers.ContainerUtil;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static com.intellij.util.ObjectUtils.tryCast;
public class MemberModel {
private final MemberType myMemberType;
private final TextRange myTextRange;
private MemberModel(@NotNull TextRange textRange, @NotNull MemberType memberType) {
myTextRange = textRange;
myMemberType = memberType;
}
public @NotNull MemberType memberType() {
return myMemberType;
}
public @NotNull TextRange textRange() {
return myTextRange;
}
public static @Nullable MemberModel create(@NotNull PsiErrorElement errorElement) {
List<PsiElement> children = new ArrayList<>();
PsiElement prevSibling = errorElement.getPrevSibling();
while (isMemberPart(prevSibling)) {
if (prevSibling instanceof PsiErrorElement) {
StreamEx.ofReversed(prevSibling.getChildren()).forEach(c -> children.add(c));
}
else {
children.add(prevSibling);
}
prevSibling = prevSibling.getPrevSibling();
}
Collections.reverse(children);
Collections.addAll(children, errorElement.getChildren());
return new MemberParser(ContainerUtil.filter(children, c -> !isWsOrComment(c))).parse();
}
private static boolean isWsOrComment(@NotNull PsiElement psiElement) {
return psiElement instanceof PsiWhiteSpace || psiElement instanceof PsiComment;
}
private static boolean isMemberPart(@Nullable PsiElement prevSibling) {
if (prevSibling == null || prevSibling instanceof PsiPackageStatement || prevSibling instanceof PsiImportList) return false;
PsiJavaToken token = tryCast(prevSibling, PsiJavaToken.class);
if (token == null) return true;
IElementType tokenType = token.getTokenType();
return tokenType != JavaTokenType.RBRACE;
}
public enum MemberType {
METHOD {
@Override
public @NotNull PsiMember create(@NotNull PsiElementFactory factory,
@NotNull String text,
@Nullable PsiElement context) {
return factory.createMethodFromText(text, context);
}
},
FIELD {
@Override
public @NotNull PsiMember create(@NotNull PsiElementFactory factory,
@NotNull String text,
@Nullable PsiElement context) {
return factory.createFieldFromText(text, context);
}
};
abstract public @NotNull PsiMember create(@NotNull PsiElementFactory factory, @NotNull String text, @Nullable PsiElement context);
}
private static class MemberParser {
private final List<PsiElement> myChildren;
private int pos;
private MemberParser(@NotNull List<PsiElement> children) {
myChildren = children;
}
private MemberModel parse() {
PsiElement startElement = nextChild();
PsiElement element = parseModifiers(startElement);
if (element == null) return null;
PsiJavaToken token = tryCast(parseModifiers(parseTypeParams(element)), PsiJavaToken.class);
if (token == null) {
MemberType memberType = parseMemberType(element);
return memberType == null ? null : new MemberModel(textRange(startElement, element), memberType);
}
boolean hasTypeParams = token != element;
token = parseType(token);
if (token == null) return null;
token = parseIdentifier(token);
if (token == null) return null;
token = tryCast(nextChild(), PsiJavaToken.class);
if (token == null) return null;
if (!hasTypeParams) {
PsiJavaToken endElement = parseField(token);
if (endElement != null) {
return new MemberModel(textRange(startElement, endElement), MemberType.FIELD);
}
}
PsiJavaToken endElement = parseMethod(token);
if (endElement != null) {
return new MemberModel(textRange(startElement, endElement), MemberType.METHOD);
}
return null;
}
private @Nullable PsiJavaToken parseMethod(@NotNull PsiJavaToken token) {
// void foo() @MyAnno {} is accepted
token = tryCast(parseModifiers(parseParams(token)), PsiJavaToken.class);
if (token == null) return null;
token = parseArrayType(token);
if (token == null) return null;
token = parseThrowsClause(token);
if (token == null) return null;
IElementType tokenType = token.getTokenType();
if (tokenType == JavaTokenType.SEMICOLON) return token;
if (tokenType != JavaTokenType.LBRACE) return null;
return findClosingBracket(token, JavaTokenType.LBRACE, JavaTokenType.RBRACE);
}
private @Nullable PsiJavaToken parseThrowsClause(@Nullable PsiJavaToken token) {
if (!(token instanceof PsiKeyword) || !PsiKeyword.THROWS.equals(token.getText())) return token;
token = tryCast(nextChild(), PsiIdentifier.class);
if (token == null) return null;
token = tryCast(parseQualifiedType(nextChild()), PsiJavaToken.class);
while (token != null && token.getTokenType() == JavaTokenType.COMMA) {
token = tryCast(nextChild(), PsiIdentifier.class);
if (token == null) return null;
token = tryCast(parseQualifiedType(nextChild()), PsiJavaToken.class);
}
return token;
}
private @Nullable PsiElement parseParams(@NotNull PsiJavaToken child) {
if (child.getTokenType() != JavaTokenType.LPARENTH) return null;
PsiJavaToken closingBracket = findClosingBracket(child, JavaTokenType.LPARENTH, JavaTokenType.RPARENTH);
if (closingBracket == null) return null;
return nextChild();
}
private @Nullable PsiElement parseModifiers(@Nullable PsiElement child) {
return child instanceof PsiModifierList ? nextChild() : child;
}
private @Nullable PsiElement parseTypeParams(@Nullable PsiElement element) {
return element instanceof PsiTypeParameterList ? nextChild() : element;
}
private @Nullable PsiJavaToken parseType(@NotNull PsiJavaToken child) {
IElementType tokenType = child.getTokenType();
boolean isPrimitiveType = ElementType.PRIMITIVE_TYPE_BIT_SET.contains(tokenType);
if (!isPrimitiveType && tokenType != JavaTokenType.IDENTIFIER) return null;
PsiElement element = nextChild();
if (!isPrimitiveType) {
element = parseQualifiedType(element);
}
else {
// in case if we have array type e.g. int @Nullable []
// now we accept invalid declarations like int @Nullable foo() {} but it's fine since we don't need precise parser
element = parseModifiers(element);
}
return parseArrayType(tryCast(element, PsiJavaToken.class));
}
private @Nullable PsiElement parseQualifiedType(@Nullable PsiElement element) {
element = parseTypeParams(parseModifiers(element));
PsiJavaToken javaToken = tryCast(element, PsiJavaToken.class);
while (javaToken != null && javaToken.getTokenType() == JavaTokenType.DOT) {
element = nextChild();
element = parseModifiers(element);
if (!(element instanceof PsiIdentifier)) return null;
element = nextChild();
// e.g. Foo.Bar<String>.Baz
// modifiers actually are not allowed in front of type parameters, but java parser generates empty modifiers list anyway
element = parseTypeParams(parseModifiers(element));
javaToken = tryCast(element, PsiJavaToken.class);
}
return element;
}
private @Nullable PsiJavaToken nextJavaToken() {
PsiElement child;
do {
child = nextChild();
}
while (child != null && !(child instanceof PsiJavaToken));
return (PsiJavaToken)child;
}
private @Nullable PsiElement nextChild() {
if (pos >= myChildren.size()) return null;
PsiElement child = myChildren.get(pos);
pos++;
return child;
}
private @Nullable PsiJavaToken findClosingBracket(@NotNull PsiElement child,
@NotNull IElementType openBracket,
@NotNull IElementType closeBracket) {
int depth = 0;
do {
PsiJavaToken token = tryCast(child, PsiJavaToken.class);
if (token != null) {
IElementType tokenType = token.getTokenType();
if (tokenType == openBracket) {
depth++;
}
else if (tokenType == closeBracket) {
depth--;
}
if (depth == 0) break;
}
child = nextChild();
}
while (child != null);
return tryCast(child, PsiJavaToken.class);
}
private @Nullable PsiJavaToken parseField(@NotNull PsiJavaToken token) {
token = parseArrayType(token);
if (token == null) return null;
IElementType tokenType = token.getTokenType();
if (tokenType == JavaTokenType.SEMICOLON) return token;
if (tokenType != JavaTokenType.EQ) return null;
while (tokenType != JavaTokenType.SEMICOLON) {
if (tokenType == JavaTokenType.LBRACE) {
token = findClosingBracket(token, JavaTokenType.LBRACE, JavaTokenType.RBRACE);
if (token == null) return null;
}
token = nextJavaToken();
if (token == null) break;
tokenType = token.getTokenType();
}
return token;
}
private @Nullable PsiJavaToken parseArrayType(@Nullable PsiJavaToken token) {
while (token != null && token.getTokenType() == JavaTokenType.LBRACKET) {
token = tryCast(nextChild(), PsiJavaToken.class);
if (token == null || token.getTokenType() != JavaTokenType.RBRACKET) return null;
token = tryCast(parseModifiers(nextChild()), PsiJavaToken.class);
}
return token;
}
private static @Nullable MemberType parseMemberType(@NotNull PsiElement element) {
if (element instanceof PsiMethod) return MemberType.METHOD;
return element instanceof PsiField ? MemberType.FIELD : null;
}
private static @NotNull TextRange textRange(@NotNull PsiElement startElement, @NotNull PsiElement endElement) {
int start = startElement.getTextRange().getStartOffset();
int end = endElement.getTextRange().getEndOffset();
return new TextRange(start, end);
}
private static @Nullable PsiJavaToken parseIdentifier(@NotNull PsiJavaToken child) {
IElementType tokenType = child.getTokenType();
return tokenType == JavaTokenType.IDENTIFIER ? child : null;
}
}
}

View File

@@ -0,0 +1,82 @@
// Copyright 2000-2021 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.daemon.impl.quickfix;
import com.intellij.codeInsight.daemon.impl.analysis.MemberModel;
import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement;
import com.intellij.java.JavaBundle;
import com.intellij.lang.jvm.JvmModifier;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.*;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
public class MoveMemberIntoClassFix extends LocalQuickFixAndIntentionActionOnPsiElement {
public MoveMemberIntoClassFix(@Nullable PsiErrorElement errorElement) {
super(errorElement);
}
@Override
public void invoke(@NotNull Project project,
@NotNull PsiFile file,
@Nullable Editor editor,
@NotNull PsiElement startElement,
@NotNull PsiElement endElement) {
if (editor == null) return;
PsiJavaFile javaFile = ObjectUtils.tryCast(file, PsiJavaFile.class);
if (javaFile == null) return;
PsiErrorElement errorElement = ObjectUtils.tryCast(startElement, PsiErrorElement.class);
if (errorElement == null) return;
MemberModel model = MemberModel.create(errorElement);
if (model == null) return;
String className = file.getVirtualFile().getNameWithoutExtension();
PsiClass psiClass = ContainerUtil.find(javaFile.getClasses(), c -> className.equals(c.getName()));
TextRange memberRange = model.textRange();
Document document = editor.getDocument();
String memberText = document.getText(memberRange);
PsiElementFactory factory = JavaPsiFacade.getElementFactory(project);
PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project);
MemberModel.MemberType memberType = model.memberType();
PsiMember member = memberType.create(factory, memberText, file);
if (psiClass == null) {
psiClass = (PsiClass)file.add(createClass(factory, className, member));
documentManager.doPostponedOperationsAndUnblockDocument(document);
}
SmartPsiElementPointer<PsiClass> classPtr = SmartPointerManager.createPointer(psiClass);
document.deleteString(memberRange.getStartOffset(), memberRange.getEndOffset());
documentManager.commitDocument(document);
psiClass = classPtr.getElement();
if (psiClass == null) return;
psiClass.add(member);
}
@Override
public @NotNull String getText() {
return getFamilyName();
}
@Override
public @NotNull String getFamilyName() {
return JavaBundle.message("intention.family.name.move.member.into.class");
}
private static @NotNull PsiClass createClass(@NotNull PsiElementFactory factory, @NotNull String className, @NotNull PsiMember member) {
PsiMethod psiMethod = ObjectUtils.tryCast(member, PsiMethod.class);
if (psiMethod == null || psiMethod.getBody() != null || psiMethod.hasModifier(JvmModifier.NATIVE)) {
return factory.createClass(className);
}
if (psiMethod.hasModifier(JvmModifier.ABSTRACT)) {
PsiClass psiClass = factory.createClass(className);
Objects.requireNonNull(psiClass.getModifierList()).setModifierProperty(PsiModifier.ABSTRACT, true);
return psiClass;
}
return factory.createInterface(className);
}
}

View File

@@ -971,4 +971,9 @@ public final class QuickFixFactoryImpl extends QuickFixFactory {
public @NotNull IntentionAction createUnimplementInterfaceAction(@NotNull PsiJavaCodeReferenceElement ref, boolean isDuplicates) {
return new UnimplementInterfaceAction(ref, isDuplicates);
}
@Override
public @NotNull IntentionAction createMoveMemberIntoClassFix(@NotNull PsiErrorElement errorElement) {
return new MoveMemberIntoClassFix(errorElement);
}
}

View File

@@ -209,6 +209,7 @@ yield.void=Expression type should not be 'void'
break.outside.switch.expr=Break out of switch expression is not allowed
continue.outside.loop=Continue outside of loop
continue.outside.switch.expr=Continue outside of enclosing switch expression
class.member.declared.outside=Class member declared outside of a class
not.loop.label=Not a loop label: ''{0}''
incompatible.modifiers=Illegal combination of modifiers: ''{0}'' and ''{1}''
modifier.not.allowed=Modifier ''{0}'' not allowed here

View File

@@ -0,0 +1,7 @@
// "Move member into class" "true"
public abstract class beforeAbstractMethodNoClass {
public abstract void doSmth();
}

View File

@@ -0,0 +1,7 @@
// "Move member into class" "true"
public class beforeFieldNoModifiers {
int foo;
}

View File

@@ -0,0 +1,7 @@
// "Move member into class" "true"
public class beforeFieldQualifiedType {
public String foo;
}

View File

@@ -0,0 +1,10 @@
// "Move member into class" "true"
class Pair<T, V> {}
public class beforeFieldWithAnnos {
@NotNull
Pair<String, String> foo;
}

View File

@@ -0,0 +1,9 @@
// "Move member into class" "true"
class Pair<T, V> {}
public class beforeFieldWithTypeParams {
Pair<String, String> foo;
}

View File

@@ -0,0 +1,9 @@
// "Move member into class" "true"
class Pair<T, V> {}
public class beforeFieldWithTypeParamsWithAnnos {
Pair<@NotNull String, String> foo;
}

View File

@@ -0,0 +1,7 @@
// "Move member into class" "true"
public interface beforeMethodNoBodyNoClass {
void foo();
}

View File

@@ -0,0 +1,10 @@
// "Move member into class" "true"
public class beforeMethodNoModifiers {
void test() {
}
}

View File

@@ -0,0 +1,10 @@
// "Move member into class" "true"
public class beforeMethodWithAnnos {
@NotNull
String withQualifiedReturnType() {
}
}

View File

@@ -0,0 +1,10 @@
// "Move member into class" "true"
public class beforeMethodWithQualifiedReturnType {
final String withQualifiedReturnType() {
}
}

View File

@@ -0,0 +1,10 @@
// "Move member into class" "true"
public class beforeMethodWithQualifiedReturnTypeWithAnnos {
@NotNull String withQualifiedReturnType() {
}
}

View File

@@ -0,0 +1,11 @@
// "Move member into class" "true"
class Pair<T, V> {}
public class beforeMethodWithTypeParams {
Pair<String, String> doSmth() {
}
}

View File

@@ -0,0 +1,7 @@
// "Move member into class" "true"
public class beforeNativeMethodNoClass {
native void doSmth();
}

View File

@@ -0,0 +1,3 @@
// "Move member into class" "true"
public abs<caret>tract void doSmth();

View File

@@ -0,0 +1,6 @@
// "Move member into class" "true"
int f<caret>oo;
public class beforeFieldNoModifiers {
}

View File

@@ -0,0 +1,6 @@
// "Move member into class" "true"
public <caret>java.lang.String foo;
public class beforeFieldQualifiedType {
}

View File

@@ -0,0 +1,8 @@
// "Move member into class" "true"
class Pair<T, V> {}
@NotNull Pair<String<caret>, String> foo;
public class beforeFieldWithAnnos {
}

View File

@@ -0,0 +1,8 @@
// "Move member into class" "true"
class Pair<T, V> {}
Pair<String<caret>, String> foo;
public class beforeFieldWithTypeParams {
}

View File

@@ -0,0 +1,8 @@
// "Move member into class" "true"
class Pair<T, V> {}
Pair<@NotNull String<caret>, String> foo;
public class beforeFieldWithTypeParamsWithAnnos {
}

View File

@@ -0,0 +1,3 @@
// "Move member into class" "true"
void foo<caret>();

View File

@@ -0,0 +1,10 @@
// "Move member into class" "true"
void test() {
<caret>
}
public class beforeMethodNoModifiers {
}

View File

@@ -0,0 +1,8 @@
// "Move member into class" "true"
@NotNull String w<caret>ithQualifiedReturnType() {
}
public class beforeMethodWithAnnos {
}

View File

@@ -0,0 +1,9 @@
// "Move member into class" "true"
final ja<caret>va.lang.String withQualifiedReturnType() {
}
public class beforeMethodWithQualifiedReturnType {
}

View File

@@ -0,0 +1,9 @@
// "Move member into class" "true"
ja<caret>va.lang.@NotNull String withQualifiedReturnType() {
}
public class beforeMethodWithQualifiedReturnTypeWithAnnos {
}

View File

@@ -0,0 +1,9 @@
// "Move member into class" "true"
class Pair<T, V> {}
Pair<String, String> doS<caret>mth() {}
public class beforeMethodWithTypeParams {
}

View File

@@ -0,0 +1,3 @@
// "Move member into class" "true"
native void <caret>doSmth();

View File

@@ -0,0 +1,12 @@
// Copyright 2000-2021 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.java.codeInsight.daemon.quickFix;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
public class MoveMemberIntoClassTest extends LightQuickFixParameterizedTestCase {
@Override
protected String getBasePath() {
return "/codeInsight/daemonCodeAnalyzer/quickFix/moveMemberIntoClass";
}
}

View File

@@ -0,0 +1,130 @@
// Copyright 2000-2021 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.java.propertyBased;
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.tree.ChildRole;
import com.intellij.psi.impl.source.tree.java.FieldElement;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import com.intellij.testFramework.propertyBased.MadTestingAction;
import com.intellij.testFramework.propertyBased.MadTestingUtil;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import one.util.streamex.StreamEx;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jetCheck.Generator;
import org.jetbrains.jetCheck.ImperativeCommand;
import org.jetbrains.jetCheck.PropertyChecker;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
public class JavaOutOfClassDefinitionPropertyTest extends LightJavaCodeInsightFixtureTestCase {
public void testMoveMethodOutOfClassAndBack() {
Supplier<MadTestingAction> fileAction =
MadTestingUtil.performOnFileContents(myFixture, PathManager.getHomePath(), f -> f.getName().endsWith(".java"),
this::doTestMoveMethodOutOfClassAndBack);
PropertyChecker.customized()
.checkScenarios(fileAction);
}
private void doTestMoveMethodOutOfClassAndBack(@NotNull ImperativeCommand.Environment env, @NotNull VirtualFile file) {
Project project = getProject();
if (!isCompilable(project, file)) return;
myFixture.openFileInEditor(file);
PsiJavaFile psiJavaFile = ObjectUtils.tryCast(myFixture.getFile(), PsiJavaFile.class);
if (psiJavaFile == null) return;
PsiClass psiClass = findClass(file, psiJavaFile);
// enum and annotation fields are not supported
if (psiClass == null || psiClass.isEnum() || psiClass.isAnnotationType()) return;
List<PsiMember> members = findMembers(psiClass);
int nMembers = members.size();
if (nMembers == 0) return;
PsiMember psiMember = env.generateValue(Generator.sampledFrom(members.toArray(PsiMember.EMPTY_ARRAY)), "Selected member %s");
SmartPsiElementPointer<PsiClass> classPtr = SmartPointerManager.createPointer(psiClass);
if (!moveMemberFromClass(project, psiClass, psiMember)) return;
psiClass = Objects.requireNonNull(classPtr.getElement());
PsiErrorElement errorElement = Objects.requireNonNull(PsiTreeUtil.getPrevSiblingOfType(psiClass, PsiErrorElement.class));
int outerMemberOffset = errorElement.getTextRange().getStartOffset();
assertTrue("Failed to find member after moving it out of a class", outerMemberOffset != -1);
moveMemberToClass(outerMemberOffset);
psiClass = classPtr.getElement();
if (psiClass == null) return;
assertSize(nMembers, findMembers(psiClass));
}
private void moveMemberToClass(int offset) {
myFixture.getEditor().getCaretModel().moveToOffset(offset);
IntentionAction intention = myFixture.findSingleIntention("Move member into class");
myFixture.launchAction(intention);
}
private boolean moveMemberFromClass(@NotNull Project project, @NotNull PsiClass psiClass, @NotNull PsiMember psiMember) {
int startOffset = psiClass.getTextRange().getStartOffset();
String memberText = psiMember.getText() + "\n";
Document document = myFixture.getDocument(psiMember.getContainingFile());
SmartPsiElementPointer<PsiMember> memberPtr = SmartPointerManager.createPointer(psiMember);
WriteCommandAction.runWriteCommandAction(project, () -> {
document.insertString(startOffset, memberText);
});
PsiDocumentManager.getInstance(project).commitAllDocuments();
PsiMember movedMember = memberPtr.getElement();
if (movedMember == null) return false;
WriteCommandAction.runWriteCommandAction(project, () -> movedMember.delete());
return true;
}
private static boolean isCompilable(@NotNull Project project, @NotNull VirtualFile file) {
if (!ProjectFileIndex.getInstance(project).isInSource(file)) return false;
String path = file.getCanonicalPath();
// for plugins testdata ProjectFileIndex#isInSource returns true
return path != null && !path.contains("testData");
}
private static boolean hasTypeParameters(@NotNull PsiMember psiMember) {
PsiTypeElement typeElement;
if (psiMember instanceof PsiMethod) {
typeElement = ((PsiMethod)psiMember).getReturnTypeElement();
}
else {
typeElement = ((PsiField)psiMember).getTypeElement();
}
return typeElement != null &&
StreamEx.ofTree((PsiElement)typeElement, e -> StreamEx.of(e.getChildren()))
.anyMatch(e -> e instanceof PsiJavaToken && ((PsiJavaToken)e).getTokenType() == JavaTokenType.LT);
}
private static boolean isMultipleFieldsDeclaration(@NotNull PsiField psiField) {
FieldElement fieldElement = ObjectUtils.tryCast(psiField.getNode(), FieldElement.class);
if (fieldElement == null) return false;
return fieldElement.findChildByRole(ChildRole.TYPE) == null || fieldElement.findChildByRole(ChildRole.CLOSING_SEMICOLON) == null;
}
private static @Nullable PsiClass findClass(@NotNull VirtualFile file, @NotNull PsiJavaFile psiJavaFile) {
PsiClass[] psiClasses = psiJavaFile.getClasses();
if (psiClasses.length != 1) return null;
PsiClass psiClass = psiClasses[0];
String className = file.getNameWithoutExtension();
return className.equals(psiClass.getName()) ? psiClass : null;
}
private static @NotNull List<PsiMember> findMembers(@NotNull PsiClass psiClass) {
Collection<? extends PsiMember> children = PsiTreeUtil.getChildrenOfAnyType(psiClass, PsiMethod.class, PsiField.class);
return ContainerUtil.filter(children, c -> (c instanceof PsiField && !isMultipleFieldsDeclaration((PsiField)c) ||
c instanceof PsiMethod && !((PsiMethod)c).isConstructor()) &&
// parser works only with simple type params, without nested type params and qualified names
!hasTypeParameters(c)
);
}
}

View File

@@ -1295,6 +1295,7 @@ intention.error.make.sealed.class.inheritors.not.in.java.file=Some of the inheri
intention.error.make.sealed.class.different.modules=Some of the inheritors are in different modules
intention.error.make.sealed.class.interface.has.no.inheritors=Interface has no inheritors
intention.make.sealed.class.task.title.set.inheritors.modifiers=Setting inheritors modifiers
intention.family.name.move.member.into.class=Move member into class
inspection.fill.permits.list.no.missing.inheritors=Sealed class has no missing inheritors
inspection.fill.permits.list.display.name=Same file subclasses are missing from permits clause of a sealed class
inspection.fill.permits.list.fix.name=Add missing subclasses to the permits clause