PY-3450 Refactoring: pull up class variables/attributes (almost done: need to check dependencies, resolve conflicts and use same functionality for "extract superclass" and "pushdown")

This commit is contained in:
Ilya.Kazakevich
2014-02-04 00:47:26 +04:00
parent 5294ae13b6
commit 955663ad6c
19 changed files with 274 additions and 39 deletions

View File

@@ -279,7 +279,7 @@ public class TemplateBuilderImpl implements TemplateBuilder {
public void run() {
final Project project = myFile.getProject();
VirtualFile file = myFile.getVirtualFile();
assert file != null;
assert file != null: "Virtual file is null for " + file;
OpenFileDescriptor descriptor = new OpenFileDescriptor(project, file);
final Editor editor = FileEditorManager.getInstance(project).openTextEditor(descriptor, true);

View File

@@ -155,6 +155,7 @@ public interface PyClass extends PsiNameIdentifierOwner, PyStatement, NameDefine
List<PyTargetExpression> getClassAttributes();
@Nullable
PyTargetExpression findClassAttribute(@NotNull String name, boolean inherited);
List<PyTargetExpression> getInstanceAttributes();

View File

@@ -38,6 +38,7 @@ import org.jetbrains.annotations.NotNull;
import static com.jetbrains.python.psi.PyUtil.sure;
/**
* TODO: Refactor and move to {@link com.jetbrains.python.refactoring.classes.PyClassRefactoringUtil#createMethod(String, com.jetbrains.python.psi.PyClass, com.jetbrains.python.psi.PyFunction.Modifier, String...)}
* Adds a method foo to class X if X.foo() is unresolved.
* User: dcheryasov
* Date: Apr 5, 2009 6:51:26 PM

View File

@@ -32,6 +32,7 @@ import com.jetbrains.python.codeInsight.PyCodeInsightSettings;
import com.jetbrains.python.codeInsight.imports.AddImportHelper;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.impl.PyBuiltinCache;
import com.jetbrains.python.psi.impl.PyFunctionBuilder;
import com.jetbrains.python.psi.impl.PyImportedModule;
import com.jetbrains.python.psi.impl.PyPsiUtils;
import com.jetbrains.python.psi.resolve.QualifiedNameFinder;
@@ -40,6 +41,9 @@ import org.jetbrains.annotations.Nullable;
import java.util.*;
import static com.jetbrains.python.psi.PyFunction.Modifier.CLASSMETHOD;
import static com.jetbrains.python.psi.PyFunction.Modifier.STATICMETHOD;
/**
* @author Dennis.Ushakov
*/
@@ -437,4 +441,41 @@ public class PyClassRefactoringUtil {
}
return null;
}
/**
* Creates class method
* @param methodName name if new method (be sure to check {@link com.jetbrains.python.PyNames} for special methods)
* @param pyClass class to add method
* @param modifier if method static or class or simple instance method (null)>
* @param parameterNames method parameters
* @return newly created method
*/
@NotNull
public static PyFunction createMethod(@NotNull final String methodName,
@NotNull final PyClass pyClass,
@Nullable final PyFunction.Modifier modifier,
@NotNull final String... parameterNames) {
final PyFunctionBuilder builder = new PyFunctionBuilder(methodName);
//TODO: Take names from codestyle?
if (modifier == null) {
builder.parameter(PyNames.CANONICAL_SELF);
}
else if (modifier == CLASSMETHOD) {
builder.parameter(PyNames.CANONICAL_CLS);
builder.decorate(PyNames.CLASSMETHOD);
}
else if (modifier == STATICMETHOD) {
builder.decorate(PyNames.STATICMETHOD);
}
for (final String parameterName : parameterNames) {
builder.parameter(parameterName);
}
final PyFunction function = builder.addFunction(pyClass.getStatementList(), LanguageLevel.getDefault());
addMethods(pyClass, new PyElement[]{function}, true);
return function;
}
}

View File

@@ -1,57 +1,40 @@
package com.jetbrains.python.refactoring.classes.membersManager;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.intellij.psi.PsiElement;
import com.jetbrains.python.psi.PyAssignmentStatement;
import com.jetbrains.python.psi.PyClass;
import com.jetbrains.python.psi.PyElement;
import com.jetbrains.python.psi.PyTargetExpression;
import com.jetbrains.python.refactoring.classes.PyClassRefactoringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Moves class attributes up
*
* @author Ilya.Kazakevich
*/
class ClassFieldsManager extends MembersManager<PyTargetExpression> {
class ClassFieldsManager extends FieldsManager {
ClassFieldsManager() {
super(PyTargetExpression.class);
super(true);
}
@NotNull
@Override
protected List<PyElement> getMembersCouldBeMoved(@NotNull final PyClass pyClass) {
return new ArrayList<PyElement>(Collections2.filter(pyClass.getClassAttributes(), new SimpleAssignmentsOnly()));
}
@Override
protected void moveMembers(@NotNull final PyClass from, @NotNull final PyClass to, @NotNull final Collection<PyTargetExpression> members) {
protected void moveMembers(@NotNull final PyClass from,
@NotNull final PyClass to,
@NotNull final Collection<PyTargetExpression> members) {
PyClassRefactoringUtil.moveFieldDeclarationToStatement(members, to.getStatementList());
}
@NotNull
@Override
public PyMemberInfo apply(@NotNull final PyElement input) {
return new PyMemberInfo(input, true, input.getText(), false, this); //TODO: Check overrides
protected boolean classHasField(@NotNull final PyClass pyClass, @NotNull final String fieldName) {
return pyClass.findClassAttribute(fieldName, true) != null;
}
private static class SimpleAssignmentsOnly implements Predicate<PyTargetExpression> {
//Support only simplest cases like CLASS_VAR = 42.
//Tuples (CLASS_VAR_1, CLASS_VAR_2) = "spam", "eggs" are not supported by now
@Override
public boolean apply(@Nullable final PyTargetExpression input) {
if (input == null) {
return false; //Filter out empties (which probably would never be here)
}
final PsiElement parent = input.getParent();
return (parent != null) && PyAssignmentStatement.class.isAssignableFrom(parent.getClass());
}
@NotNull
@Override
protected List<PyTargetExpression> getFieldsByClass(@NotNull PyClass pyClass) {
return pyClass.getClassAttributes();
}
}

View File

@@ -0,0 +1,96 @@
package com.jetbrains.python.refactoring.classes.membersManager;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.Lists;
import com.intellij.psi.PsiElement;
import com.jetbrains.python.psi.PyAssignmentStatement;
import com.jetbrains.python.psi.PyClass;
import com.jetbrains.python.psi.PyElement;
import com.jetbrains.python.psi.PyTargetExpression;
import com.jetbrains.python.refactoring.classes.PyClassRefactoringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.List;
/**
* Parent of all field-based plugins (like class fields, instance fields and so on)
* @author Ilya.Kazakevich
*/
abstract class FieldsManager extends MembersManager<PyTargetExpression> {
private static final SimpleAssignmentsOnly SIMPLE_ASSIGNMENTS_ONLY = new SimpleAssignmentsOnly();
private final boolean myStatic;
/**
* @param isStatic is field static or not?
*/
protected FieldsManager(final boolean isStatic) {
super(PyTargetExpression.class);
myStatic = isStatic;
}
@NotNull
@Override
protected List<PyElement> getMembersCouldBeMoved(@NotNull final PyClass pyClass) {
return Lists.<PyElement>newArrayList(Collections2.filter(getFieldsByClass(pyClass), SIMPLE_ASSIGNMENTS_ONLY));
}
/**
* Checks if class has fields. Only child may know how to obtain field
* @param pyClass class to check
* @param fieldName field name
* @return true if has one
*/
protected abstract boolean classHasField(@NotNull PyClass pyClass, @NotNull String fieldName);
/**
* Returns all fields by class. Only child may know how to obtain fields
* @param pyClass class to check
* @return list of fields in target expression (declaration) form
*/
@NotNull
protected abstract List<PyTargetExpression> getFieldsByClass(@NotNull PyClass pyClass);
@NotNull
@Override
public PyMemberInfo apply(@NotNull final PyElement input) {
return new PyMemberInfo(input, myStatic, input.getText(), isOverrides((PyTargetExpression)input), this);
}
@Nullable
private Boolean isOverrides(@NotNull final PyTargetExpression input) {
final PyClass aClass = input.getContainingClass();
final String name = input.getName();
if (name == null) {
return null; //Field with out of name can't override something
}
assert aClass != null : "Target expression declared outside of class:" + input;
return classHasField(aClass, name) ? true : null;
}
@Override
protected void moveMembers(@NotNull final PyClass from,
@NotNull final PyClass to,
@NotNull final Collection<PyTargetExpression> members) {
PyClassRefactoringUtil.moveFieldDeclarationToStatement(members, to.getStatementList());
}
private static class SimpleAssignmentsOnly implements Predicate<PyTargetExpression> {
//Support only simplest cases like CLASS_VAR = 42.
//Tuples (CLASS_VAR_1, CLASS_VAR_2) = "spam", "eggs" are not supported by now
@Override
public boolean apply(@Nullable final PyTargetExpression input) {
if (input == null) {
return false; //Filter out empties (which probably would never be here)
}
final PsiElement parent = input.getParent();
return (parent != null) && PyAssignmentStatement.class.isAssignableFrom(parent.getClass());
}
}
}

View File

@@ -0,0 +1,46 @@
package com.jetbrains.python.refactoring.classes.membersManager;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.refactoring.classes.PyClassRefactoringUtil;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.List;
/**
* @author Ilya.Kazakevich
*/
class InstanceFieldsManager extends FieldsManager {
InstanceFieldsManager() {
super(false);
}
@Override
protected void moveMembers(@NotNull final PyClass from,
@NotNull final PyClass to,
@NotNull final Collection<PyTargetExpression> members) {
//We need __init__ method, and if there is no any -- we need to create it
PyFunction initMethod = to.findMethodByName(PyNames.INIT, false);
if (initMethod == null) {
initMethod = PyClassRefactoringUtil.createMethod(PyNames.INIT, to, null);
}
final PyStatementList statementList = initMethod.getStatementList();
if (statementList == null) {
return; //TODO: Investigate how could it be
}
PyClassRefactoringUtil.moveFieldDeclarationToStatement(members, statementList);
}
@Override
protected boolean classHasField(@NotNull final PyClass pyClass, @NotNull final String fieldName) {
return pyClass.findInstanceAttribute(fieldName, true) != null;
}
@NotNull
@Override
protected List<PyTargetExpression> getFieldsByClass(@NotNull final PyClass pyClass) {
return pyClass.getInstanceAttributes();
}
}

View File

@@ -31,7 +31,7 @@ public abstract class MembersManager<T extends PyElement> implements Function<Py
* List of managers. Class delegates all logic to them.
*/
private static final Collection<? extends MembersManager<?>> MANAGERS =
Arrays.asList(new MethodsManager(), new SuperClassesManager(), new ClassFieldsManager());
Arrays.asList(new MethodsManager(), new SuperClassesManager(), new ClassFieldsManager(), new InstanceFieldsManager());
private static final PyMemberExtractor PY_MEMBER_EXTRACTOR = new PyMemberExtractor();
@NotNull
@@ -121,7 +121,8 @@ public abstract class MembersManager<T extends PyElement> implements Function<Py
/**
* Filters out named elements (ones that subclasses {@link com.intellij.psi.PsiNamedElement}) and {@link com.jetbrains.python.psi.PyElement})
* that are null or has null name.
* You need it sometimes when code has errors (i.e. bad formatted code with annotation may treat annotation as method with null name)
* You need it sometimes when code has errors (i.e. bad formatted code with annotation may treat annotation as method with null name.
* note: we should probably throw exceptions in such cases and display "refactoring not available" window in handler)
*
* @param elementsToFilter collection of elements to filter
* @param <T> element type
@@ -141,7 +142,12 @@ public abstract class MembersManager<T extends PyElement> implements Function<Py
*/
protected abstract void moveMembers(@NotNull PyClass from, @NotNull PyClass to, @NotNull Collection<T> members);
//TODO: Doc
/**
* Creates {@link com.jetbrains.python.refactoring.classes.membersManager.PyMemberInfo} from {@link com.jetbrains.python.psi.PyElement}
* This process is plugin-specific and should be implemented in each plugin
* @param input element
* @return member info
*/
@SuppressWarnings("NullableProblems") //IDEA-120100
@NotNull
@Override

View File

@@ -38,6 +38,7 @@ public class PyPullUpConflictsUtil {
for (PyMemberInfo info : infos) {
PsiElement member = info.getMember();
boolean isConflict = false;
//TODO: Delegate to MemeberManagers here
if (member instanceof PyFunction) {
final String name = ((PyFunction)member).getName();
if (name == null) continue;

View File

@@ -31,6 +31,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
/**
* Pull-up presenter implementation
@@ -87,12 +88,13 @@ class PyPullUpPresenterImpl extends AbstractUsesDependencyMemberInfoModel<PyElem
public boolean isMemberEnabled(PyMemberInfo member) {
PyClass currentSuperClass = myView.getSelectedParent();
if (member.getMember() instanceof PyClass) {
//TODO: Delegate to Memebers Managers
PyClass memberClass = (PyClass)member.getMember();
if (memberClass.isSubclass(currentSuperClass) || currentSuperClass.isSubclass(memberClass)) {
return false; //Class is already parent of superclass
}
}
if (! PyPullUpConflictsUtil.checkConflicts(Arrays.asList(member), myView.getSelectedParent()).isEmpty()) {
if (! PyPullUpConflictsUtil.checkConflicts(Collections.singletonList(member), myView.getSelectedParent()).isEmpty()) {
return false; //Member has conflict
}
return (!myStorage.getDuplicatedMemberInfos(currentSuperClass).contains(member)) && member.getMember() != currentSuperClass;

View File

@@ -59,6 +59,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* * TODO: Merge with {@link com.jetbrains.python.refactoring.classes.PyClassRefactoringUtil#createMethod(String, com.jetbrains.python.psi.PyClass, com.jetbrains.python.psi.PyFunction.Modifier, String...)}
* @author oleg
*/
public class PyExtractMethodUtil {

View File

@@ -0,0 +1,9 @@
class Parent:
def __init__(self):
self.instance_field = "eggs"
class Child(Parent):
def __init__(self):
Parent2.__init__(self)

View File

@@ -0,0 +1,9 @@
class Parent:
pass
class Child(Parent):
def __init__(self):
Parent2.__init__(self)
self.instance_field = "eggs"

View File

@@ -0,0 +1,9 @@
class Parent:
def __init__(self):
self.instance_field = "eggs"
class Child(Parent):
def __init__(self):
Parent2.__init__(self)

View File

@@ -0,0 +1,10 @@
class Parent:
def __init__(self):
pass
class Child(Parent):
def __init__(self):
Parent2.__init__(self)
self.instance_field = "eggs"

View File

@@ -29,7 +29,11 @@ class NoMembers(object):
class BadMro(MainParent, object, SubParent1, SubParent2):
pass
class SomeMembersDisabled(SubParent1, date): #SubParent1 is disabled
class HugeChild(SubParent1, date): #SubParent1 is disabled
def __init__(self):
self.instance_field_1 = 42
self.instance_field_2 = 100500
CLASS_FIELD = 42
(CLASS_FIELD_A,CLASS_FIELD_B) = (42,100500) #We do not support tuples in class assignments for now (see ClassFieldsManager)
def foo(self): #should be disabled

View File

@@ -20,6 +20,7 @@ import com.jetbrains.python.fixtures.PyTestCase;
import com.jetbrains.python.psi.PyClass;
import com.jetbrains.python.psi.PyElement;
import com.jetbrains.python.psi.PyFunction;
import com.jetbrains.python.psi.PyTargetExpression;
import com.jetbrains.python.psi.stubs.PyClassNameIndex;
import org.hamcrest.Matchers;
import org.jetbrains.annotations.NotNull;
@@ -56,7 +57,12 @@ public abstract class PyClassRefactoringTest extends PyTestCase {
}
private PyElement findField(final String className, final String memberName) {
return findClass(className).findClassAttribute(memberName, false);
final PyClass aClass = findClass(className);
final PyTargetExpression attribute = aClass.findClassAttribute(memberName, false);
if (attribute != null) {
return attribute;
}
return aClass.findInstanceAttribute(memberName, false);
}
private PyFunction findMethod(final String className, final String name) {

View File

@@ -57,6 +57,13 @@ public class PyPullUpTest extends PyClassRefactoringTest {
doHelperTest("Child2", "#CLASS_VAR", "Parent2");
}
public void testMoveInstanceAttributesSimple() {
doHelperTest("Child", "#instance_field", "Parent");
}
public void testMoveInstanceAttributesNoInit() {
doHelperTest("Child", "#instance_field", "Parent");
}
public void testMultiFile() { // PY-2810
doMultiFileTest();
}

View File

@@ -86,10 +86,10 @@ public class PyPullUpPresenterTest extends PyTestCase {
}
/**
* Checks that some members are not allowed
* Checks that some members are not allowed, while others are
*/
public void testDisabledMembers() throws Exception {
PyPullUpPresenterImpl sut = configureByClass("SomeMembersDisabled");
public void testMembers() throws Exception {
PyPullUpPresenterImpl sut = configureByClass("HugeChild");
EasyMock.expect(myView.getSelectedParent()).andReturn(getClassByName("SubParent1")).anyTimes();
myMocksControl.replay();
@@ -104,11 +104,14 @@ public class PyPullUpPresenterTest extends PyTestCase {
final Matcher<Iterable<? extends Entry>> matcher = Matchers
.containsInAnyOrder(new Entry("extends date", true, false),
new Entry("CLASS_FIELD", true, true),
new Entry("__init__(self)", true, false),
new Entry("extends SubParent1", false, false),
new Entry("foo(self)", false, false),
new Entry("bar(self)", true, false),
new Entry("static_1(cls)", true, true),
new Entry("static_2()", true, true),
new Entry("self.instance_field_1", true, false),
new Entry("self.instance_field_2", true, false),
new Entry("bad_method()", true, false)
);
Assert.assertThat("Wrong members or their states", memberNamesAndStatus, matcher);