mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Detect parameters of bound static methods (PY-50). Added tests.
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user