Detect parameters of bound static methods (PY-50). Added tests.

This commit is contained in:
Dmitry Cheryasov
2010-05-07 18:55:34 +03:00
parent 80455d5c18
commit 55b6e405a6
7 changed files with 146 additions and 32 deletions
@@ -4,6 +4,7 @@ import com.intellij.psi.PsiPolyVariantReference;
import com.intellij.psi.ResolveResult;
import com.jetbrains.python.psi.impl.PyQualifiedName;
import com.jetbrains.python.psi.resolve.PyResolveContext;
import com.jetbrains.python.psi.resolve.QualifiedResolveResult;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -21,7 +22,7 @@ public interface PyReferenceExpression extends PyQualifiedExpression {
* <i>Note: will return null if the assignment chain ends in a target of a non-assignment statement such as 'for'.</i>
*/
@NotNull
ResolveResult followAssignmentsChain();
QualifiedResolveResult followAssignmentsChain();
@Nullable
PyQualifiedName asQualifiedName();
@@ -3,10 +3,10 @@ package com.jetbrains.python.psi.impl;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiNamedElement;
import com.intellij.psi.ResolveResult;
import com.jetbrains.python.PyNames;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.resolve.ImplicitResolveResult;
import com.jetbrains.python.psi.resolve.QualifiedResolveResult;
import com.jetbrains.python.psi.types.PyClassType;
import com.jetbrains.python.psi.types.PyType;
import com.jetbrains.python.psi.types.TypeEvalContext;
@@ -78,7 +78,7 @@ public class PyCallExpressionHelper {
boolean is_constructor_call = false;
if (callee instanceof PyReferenceExpression) {
PyReferenceExpression ref = (PyReferenceExpression)callee;
ResolveResult resolveResult = ref.followAssignmentsChain();
QualifiedResolveResult resolveResult = ref.followAssignmentsChain();
PsiElement resolved = resolveResult.getElement();
if (resolved instanceof PyClass) {
resolved = ((PyClass)resolved).findInitOrNew(true); // class to constructor call
@@ -97,7 +97,14 @@ public class PyCallExpressionHelper {
}
if (resolved instanceof PyFunction) {
EnumSet<PyFunction.Flag> flags = EnumSet.noneOf(PyFunction.Flag.class);
int implicit_offset = getImplicitArgumentCount(us.getCallee(), (PyFunction) resolved, wrapped_flag, flags);
PyExpression last_qualifier = resolveResult.getLastQualifier();
final PyExpression call_reference = us.getCallee();
boolean is_by_instance = isByInstance(call_reference);
if (last_qualifier != null) {
PyType qualifier_type = last_qualifier.getType(TypeEvalContext.fast()); // NOTE: ...or slow()?
is_by_instance |= (qualifier_type != null && qualifier_type instanceof PyClassType && !((PyClassType)qualifier_type).isDefinition());
}
int implicit_offset = getImplicitArgumentCount(call_reference, (PyFunction) resolved, wrapped_flag, flags, is_by_instance);
if (! is_constructor_call && PyNames.NEW.equals(((PyFunction)resolved).getName())) {
implicit_offset = Math.min(implicit_offset-1, 0); // case of Class.__new__
}
@@ -108,27 +115,45 @@ public class PyCallExpressionHelper {
return null;
}
/**
* Calls the {@link #getImplicitArgumentCount(PyExpression, PyFunction, PyFunction.Flag, EnumSet<PyFunction.Flag>, boolean) full version}
* with null flags and with isByInstance inferred directly from call site (won't work with reassigned bound methods).
* @param callReference the call site, where arguments are given.
* @param functionBeingCalled resolved method which is being called; plain functions are OK but make little sense.
* @return a non-negative number of parameters that are implicit to this call.
*/
public static int getImplicitArgumentCount(final PyExpression callReference, PyFunction functionBeingCalled) {
return getImplicitArgumentCount(callReference, functionBeingCalled, null, null);
return getImplicitArgumentCount(callReference, functionBeingCalled, null, null, isByInstance(callReference));
}
/**
* Finds how many arguments are implicit in a given call.
* @param callReference the call site, where arguments are given.
* @param method resolved method which is being called; plain functions are OK but make little sense.
* @param wrappedFlag value of {@link PyFunction.Flag#WRAPPED} if known.
* @param flags set of flags to be <i>updated</i> by this call; wrappedFlag's value ends up here, too.
* @param isByInstance true if the call is known to be by instance (not by class).
* @return a non-negative number of parameters that are implicit to this call. E.g. for a typical method call 1 is returned
* because one parameter ('self') is implicit.
*/
private static int getImplicitArgumentCount(final PyExpression callReference,
PyFunction method,
@Nullable PyFunction.Flag wrapped_flag,
@Nullable EnumSet<PyFunction.Flag> flags) {
@Nullable PyFunction.Flag wrappedFlag,
@Nullable EnumSet<PyFunction.Flag> flags,
boolean isByInstance
) {
int implicit_offset = 0;
boolean is_by_instance = isByInstance(callReference);
if (is_by_instance) implicit_offset += 1;
if (isByInstance) implicit_offset += 1;
// wrapped flags?
if (wrapped_flag != null) {
if (wrappedFlag != null) {
if (flags != null) {
flags.add(wrapped_flag);
flags.add(wrappedFlag);
flags.add(PyFunction.Flag.WRAPPED);
}
if (wrapped_flag == PyFunction.Flag.STATICMETHOD && implicit_offset > 0) implicit_offset -= 1; // might have marked it as implicit 'self'
if (wrapped_flag == PyFunction.Flag.CLASSMETHOD && ! is_by_instance) implicit_offset += 1; // Both Foo.method() and foo.method() have implicit the first arg
if (wrappedFlag == PyFunction.Flag.STATICMETHOD && implicit_offset > 0) implicit_offset -= 1; // might have marked it as implicit 'self'
if (wrappedFlag == PyFunction.Flag.CLASSMETHOD && ! isByInstance) implicit_offset += 1; // Both Foo.method() and foo.method() have implicit the first arg
}
if (! is_by_instance && PyNames.NEW.equals(method.getName())) implicit_offset += 1; // constructor call
if (! isByInstance && PyNames.NEW.equals(method.getName())) implicit_offset += 1; // constructor call
// decorators?
if (PyNames.INIT.equals(method.getName())) {
String refName = callReference instanceof PyReferenceExpression
@@ -151,13 +176,13 @@ public class PyCallExpressionHelper {
if (flags != null) {
flags.add(PyFunction.Flag.STATICMETHOD);
}
if (is_by_instance && implicit_offset > 0) implicit_offset -= 1; // might have marked it as implicit 'self'
if (isByInstance && implicit_offset > 0) implicit_offset -= 1; // might have marked it as implicit 'self'
}
else if (PyNames.CLASSMETHOD.equals(deconame)) {
if (flags != null) {
flags.add(PyFunction.Flag.CLASSMETHOD);
}
if (! is_by_instance) implicit_offset += 1; // Both Foo.method() and foo.method() have implicit the first arg
if (! isByInstance) implicit_offset += 1; // Both Foo.method() and foo.method() have implicit the first arg
}
// else could be custom decorator processing
}
@@ -16,6 +16,7 @@ import com.jetbrains.python.console.pydev.PydevConsoleCommunication;
import com.jetbrains.python.psi.*;
import com.jetbrains.python.psi.resolve.PyResolveContext;
import com.jetbrains.python.psi.resolve.PyResolveUtil;
import com.jetbrains.python.psi.resolve.QualifiedResolveResult;
import com.jetbrains.python.psi.types.*;
import com.jetbrains.python.refactoring.PyDefUseUtil;
import org.jetbrains.annotations.NotNull;
@@ -97,10 +98,13 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere
}
private final QualifiedResolveResult EMPTY_RESULT = new QualifiedResolveResultEmpty();
@NotNull
public ResolveResult followAssignmentsChain() {
public QualifiedResolveResult followAssignmentsChain() {
PyReferenceExpression seeker = this;
ResolveResult ret = null;
QualifiedResolveResult ret = null;
PyExpression last_qualifier = null;
SEARCH:
while (ret == null) {
ResolveResult[] targets = seeker.getReference().multiResolve(false);
@@ -110,28 +114,19 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere
PyExpression assigned_from = ((PyTargetExpression)elt).findAssignedValue();
if (assigned_from instanceof PyReferenceExpression) {
seeker = (PyReferenceExpression)assigned_from;
if (seeker.getQualifier() != null) last_qualifier = seeker.getQualifier();
continue SEARCH;
}
else if (assigned_from != null) ret = new PsiElementResolveResult(assigned_from);
else if (assigned_from != null) ret = new QualifiedResolveResultImpl(assigned_from, last_qualifier);
}
else if (ret == null && elt instanceof PyElement) { // remember this result, but a further reference may be the next resolve result
ret = target;
ret = new QualifiedResolveResultImpl(target.getElement(), target.isValidResult(), last_qualifier);
}
}
// all resolve results checked, reassignment not detected, nothing more to do
break;
}
if (ret == null) {
ret = new ResolveResult() {
public PsiElement getElement() {
return null;
}
public boolean isValidResult() {
return false;
}
};
}
if (ret == null) ret = EMPTY_RESULT;
return ret;
}
@@ -291,4 +286,44 @@ public class PyReferenceExpressionImpl extends PyElementImpl implements PyRefere
return null;
}
private static class QualifiedResolveResultImpl extends PsiElementResolveResult implements QualifiedResolveResult {
// a trivial implementation
private PyExpression myLastQualifier;
QualifiedResolveResultImpl(@NotNull PsiElement element, PyExpression lastQualifier) {
super(element);
myLastQualifier = lastQualifier;
}
public QualifiedResolveResultImpl(@NotNull PsiElement element, boolean validResult, PyExpression lastQualifier) {
super(element, validResult);
myLastQualifier = lastQualifier;
}
public PyExpression getLastQualifier() {
return myLastQualifier;
}
}
private static class QualifiedResolveResultEmpty implements QualifiedResolveResult {
// a trivial implementation
public QualifiedResolveResultEmpty() {
}
public PyExpression getLastQualifier() {
return null;
}
public PsiElement getElement() {
return null;
}
public boolean isValidResult() {
return false;
}
}
}
@@ -0,0 +1,11 @@
class A(object):
def foo(self, a, b):
pass
moo = foo
ff = A().moo
f = ff
f(<arg1>1, <arg2>2)
@@ -0,0 +1,7 @@
class A(object):
def foo(self, a, b):
pass
f = A().foo
f(<arg1>1, <arg2>2)
@@ -0,0 +1,8 @@
class A(object):
@staticmethod
def foo(a, b):
pass
f = A().foo
f(<arg1>1, <arg2>2)
@@ -271,6 +271,33 @@ public class PyParameterInfoTest extends LightMarkedTestCase {
feignCtrlP(marks.get("<arg1>").getTextOffset()).check("self,one", new String[]{"one"}, new String[]{"self,"});
}
public void testBoundMethodSimple() throws Exception {
Map<String, PsiElement> marks = loadTest();
assertEquals("Test data sanity", marks.size(), 2);
feignCtrlP(marks.get("<arg1>").getTextOffset()).check("self,a,b", new String[]{"a,"}, new String[]{"self,"});
feignCtrlP(marks.get("<arg2>").getTextOffset()).check("self,a,b", new String[]{"b"}, new String[]{"self,"});
}
public void testBoundMethodReassigned() throws Exception {
Map<String, PsiElement> marks = loadTest();
assertEquals("Test data sanity", marks.size(), 2);
feignCtrlP(marks.get("<arg1>").getTextOffset()).check("self,a,b", new String[]{"a,"}, new String[]{"self,"});
feignCtrlP(marks.get("<arg2>").getTextOffset()).check("self,a,b", new String[]{"b"}, new String[]{"self,"});
}
public void testBoundMethodStatic() throws Exception {
Map<String, PsiElement> marks = loadTest();
assertEquals("Test data sanity", marks.size(), 2);
feignCtrlP(marks.get("<arg1>").getTextOffset()).check("a,b", new String[]{"a,"});
feignCtrlP(marks.get("<arg2>").getTextOffset()).check("a,b", new String[]{"b"});
}
// TODO: add method tests with decorators when a mock SDK is available
/**
@@ -433,7 +460,7 @@ public class PyParameterInfoTest extends LightMarkedTestCase {
disabled_set.addAll(Arrays.asList(disabled));
for (int i=0; i < myTexts.length; i += 1) {
if (myFlags[i].contains(Flag.DISABLE) && !disabled_set.contains(myTexts[i])) {
wrongs.append("Highlighted unexpected '").append(myTexts[i]).append("'. ");
wrongs.append("Highlighted a disabled '").append(myTexts[i]).append("'. ");
}
}
for (int i=0; i < myTexts.length; i += 1) {