mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-06 20:39:40 +07:00
MemberModel: detect possible out of class definitions and suggest to move them into class (IDEA-258839)
GitOrigin-RevId: 7708fdc7b3191bcd0ba46767160c4ec0b5a179da
This commit is contained in:
committed by
intellij-monorepo-bot
parent
49252c34ab
commit
ccc5077467
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
|
||||
|
||||
public abstract class beforeAbstractMethodNoClass {
|
||||
public abstract void doSmth();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
|
||||
|
||||
public class beforeFieldNoModifiers {
|
||||
int foo;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
|
||||
|
||||
public class beforeFieldQualifiedType {
|
||||
public String foo;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
class Pair<T, V> {}
|
||||
|
||||
|
||||
|
||||
public class beforeFieldWithAnnos {
|
||||
@NotNull
|
||||
Pair<String, String> foo;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
class Pair<T, V> {}
|
||||
|
||||
|
||||
|
||||
public class beforeFieldWithTypeParams {
|
||||
Pair<String, String> foo;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
class Pair<T, V> {}
|
||||
|
||||
|
||||
|
||||
public class beforeFieldWithTypeParamsWithAnnos {
|
||||
Pair<@NotNull String, String> foo;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
|
||||
|
||||
public interface beforeMethodNoBodyNoClass {
|
||||
void foo();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
|
||||
|
||||
public class beforeMethodNoModifiers {
|
||||
void test() {
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
|
||||
|
||||
|
||||
public class beforeMethodWithAnnos {
|
||||
@NotNull
|
||||
String withQualifiedReturnType() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
|
||||
|
||||
|
||||
public class beforeMethodWithQualifiedReturnType {
|
||||
final String withQualifiedReturnType() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
|
||||
|
||||
|
||||
public class beforeMethodWithQualifiedReturnTypeWithAnnos {
|
||||
@NotNull String withQualifiedReturnType() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
class Pair<T, V> {}
|
||||
|
||||
|
||||
|
||||
public class beforeMethodWithTypeParams {
|
||||
|
||||
Pair<String, String> doSmth() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
|
||||
|
||||
public class beforeNativeMethodNoClass {
|
||||
native void doSmth();
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
public abs<caret>tract void doSmth();
|
||||
@@ -0,0 +1,6 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
int f<caret>oo;
|
||||
|
||||
public class beforeFieldNoModifiers {
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
public <caret>java.lang.String foo;
|
||||
|
||||
public class beforeFieldQualifiedType {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
class Pair<T, V> {}
|
||||
|
||||
@NotNull Pair<String<caret>, String> foo;
|
||||
|
||||
public class beforeFieldWithAnnos {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
class Pair<T, V> {}
|
||||
|
||||
Pair<String<caret>, String> foo;
|
||||
|
||||
public class beforeFieldWithTypeParams {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
class Pair<T, V> {}
|
||||
|
||||
Pair<@NotNull String<caret>, String> foo;
|
||||
|
||||
public class beforeFieldWithTypeParamsWithAnnos {
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
void foo<caret>();
|
||||
@@ -0,0 +1,10 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
void test() {
|
||||
<caret>
|
||||
|
||||
|
||||
}
|
||||
|
||||
public class beforeMethodNoModifiers {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
|
||||
@NotNull String w<caret>ithQualifiedReturnType() {
|
||||
}
|
||||
|
||||
public class beforeMethodWithAnnos {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
|
||||
final ja<caret>va.lang.String withQualifiedReturnType() {
|
||||
|
||||
}
|
||||
|
||||
public class beforeMethodWithQualifiedReturnType {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
|
||||
ja<caret>va.lang.@NotNull String withQualifiedReturnType() {
|
||||
|
||||
}
|
||||
|
||||
public class beforeMethodWithQualifiedReturnTypeWithAnnos {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
class Pair<T, V> {}
|
||||
|
||||
Pair<String, String> doS<caret>mth() {}
|
||||
|
||||
public class beforeMethodWithTypeParams {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
// "Move member into class" "true"
|
||||
|
||||
native void <caret>doSmth();
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user