Adds proper inspection of decorator argument lists. Changes parsing slightly, updates call analysis.

Fixes PY-97.
This commit is contained in:
Dmitry Cheryasov
2009-01-08 13:27:28 +03:00
parent 7366967bf5
commit f3103c6f1f
14 changed files with 157 additions and 55 deletions
@@ -28,6 +28,7 @@ INSP.duplicate.doublestar.arg=Duplicate **arg
INSP.cannot.appear.past.keyword.arg=Cannot appear past keyword arguments
INSP.unexpected.arg=Unexpected argument
INSP.parameter.$0.unfilled=Parameter ''{0}'' unfilled
INSP.func.$0.lacks.first.arg=Function ''{0}'' lacks a positional argument
# PyMethodParametersInspection
INSP.NAME.problematic.first.parameter=Methods having troubles with first parameter
@@ -46,6 +47,10 @@ INSP.module.$0.not.found=Module ''{0}'' not found
INSP.unresolved.ref.$0=Unresolved reference ''{0}''
INSP.unresolved.ref.$0.for.class.$1=Unresolved attribute reference ''{0}'' for class ''{1}''
# ReturnValueFromInitInspection
INSP.NAME.init.return=__init__ method that returns a value
INSP.cant.return.value.from.init=Cannot return a value from __init__
### Annotators ###
ANN.deleting.none=deleting None
ANN.assign.to.none=assignment to None
@@ -3,7 +3,6 @@ package com.jetbrains.python;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.lang.parameterInfo.*;
import com.jetbrains.python.psi.*;
import static com.jetbrains.python.psi.PyCallExpression.Flag;
import static com.jetbrains.python.psi.PyCallExpression.PyMarkedFunction;
import org.jetbrains.annotations.NotNull;
@@ -103,9 +102,9 @@ public class PyParameterInfoHandler implements ParameterInfoHandler<PyArgumentLi
// ^^ gotta hate the covariance issues
for (int i =0; i < flags.length; i += 1) flags[i] = EnumSet.noneOf(ParameterInfoUIContextEx.Flag.class);
if (marked.getFlags().contains(Flag.IMPLICIT_FIRST_ARG)) {
//arg_index -= 1; // argument 0 is parameter 1, thus kipping para,eter 0 which is 'self'
flags[0].add(ParameterInfoUIContextEx.Flag.DISABLE); // show but mark as absent
// disable implicit params
for (int i=0; i < marked.getImplicitOffset(); i +=1) {
flags[i].add(ParameterInfoUIContextEx.Flag.DISABLE); // show but mark as absent
}
int cur_arg_index = 0;
for (PyExpression arg : arglist.getArguments()) {
@@ -5,11 +5,9 @@ import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.lang.ASTNode;
import com.intellij.psi.PsiElementVisitor;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.PyBundle;
import com.jetbrains.python.psi.PyArgumentList;
import com.jetbrains.python.psi.PyExpression;
import com.jetbrains.python.psi.PyParameter;
import com.jetbrains.python.PyTokenTypes;
import com.jetbrains.python.psi.*;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
@@ -64,8 +62,7 @@ public class PyArgumentListInspection extends LocalInspectionTool {
@Override
public void visitPyArgumentList(final PyArgumentList node) {
// analyze
/*ArgumentAnalysisResult result = analyzeArgumentList(node);*/
PyArgumentList.AnalysisResult result = node.analyzeCall();
inspectPyArgumentList(node, getHolder());
/*
// X XX debug
PyCallExpression call_ex = node.getCallExpression();
@@ -105,36 +102,70 @@ public class PyArgumentListInspection extends LocalInspectionTool {
System.out.println();
// \\
*/
// show argument problems
for (Map.Entry<PyExpression, EnumSet<PyArgumentList.ArgFlag>> arg_entry : result.getArgumentFlags().entrySet()) {
EnumSet<PyArgumentList.ArgFlag> flags = arg_entry.getValue();
if (!flags.isEmpty()) { // something's wrong
PyExpression arg = arg_entry.getKey();
if (flags.contains(PyArgumentList.ArgFlag.IS_DUP)) {
registerProblem(arg, PyBundle.message("INSP.duplicate.argument"));
}
if (flags.contains(PyArgumentList.ArgFlag.IS_DUP_KWD)) {
registerProblem(arg, PyBundle.message("INSP.duplicate.doublestar.arg"));
}
if (flags.contains(PyArgumentList.ArgFlag.IS_DUP_TUPLE)) {
registerProblem(arg, PyBundle.message("INSP.duplicate.star.arg"));
}
if (flags.contains(PyArgumentList.ArgFlag.IS_POS_PAST_KWD)) {
registerProblem(arg, PyBundle.message("INSP.cannot.appear.past.keyword.arg"));
}
if (flags.contains(PyArgumentList.ArgFlag.IS_UNMAPPED)) {
registerProblem(arg, PyBundle.message("INSP.unexpected.arg"));
}
@Override
public void visitPyDecoratorList(final PyDecoratorList node) {
PyDecorator[] decos = node.getDecorators();
for (PyDecorator deco : decos) {
if (! deco.hasArgumentList()) {
// empty arglist; deco function must have a non-kwarg first arg
PyCallExpression.PyMarkedFunction mkfunc = deco.resolveCallee();
if (mkfunc != null) {
PyFunction decofunc = mkfunc.getFunction();
int first_param_offset = mkfunc.getImplicitOffset();
PyParameter[] params = decofunc.getParameterList().getParameters();
if (params.length < first_param_offset || params[first_param_offset-1].isKeywordContainer()) {
// no paramaters left to pass function implicitly, or wrong param type
registerProblem(deco, PyBundle.message("INSP.func.$0.lacks.first.arg", decofunc.getName()));
}
else {
// possible unfilled params
for (int i=first_param_offset; i < params.length; i += 1) {
PyParameter par = params[i];
if (! par.isKeywordContainer() && ! par.isPositionalContainer() && (par.getDefaultValue() == null)) {
registerProblem(deco, PyBundle.message("INSP.parameter.$0.unfilled", par.getName()));
}
}
}
}
}
// else: this case is handled by arglist visitor
}
// show unfilled params
ASTNode our_node = node.getNode();
if (our_node != null) {
ASTNode close_paren = our_node.findChildByType(PyTokenTypes.RPAR);
if (close_paren != null) {
for (PyParameter param : result.getUnmappedParams()) {
registerProblem(close_paren.getPsi(), PyBundle.message("INSP.parameter.$0.unfilled", param.getName()));
}
}
}
public static void inspectPyArgumentList(PyArgumentList node, ProblemsHolder holder) {
PyArgumentList.AnalysisResult result = node.analyzeCall();
for (Map.Entry<PyExpression, EnumSet<PyArgumentList.ArgFlag>> arg_entry : result.getArgumentFlags().entrySet()) {
EnumSet<PyArgumentList.ArgFlag> flags = arg_entry.getValue();
if (!flags.isEmpty()) { // something's wrong
PyExpression arg = arg_entry.getKey();
if (flags.contains(PyArgumentList.ArgFlag.IS_DUP)) {
holder.registerProblem(arg, PyBundle.message("INSP.duplicate.argument"));
}
if (flags.contains(PyArgumentList.ArgFlag.IS_DUP_KWD)) {
holder.registerProblem(arg, PyBundle.message("INSP.duplicate.doublestar.arg"));
}
if (flags.contains(PyArgumentList.ArgFlag.IS_DUP_TUPLE)) {
holder.registerProblem(arg, PyBundle.message("INSP.duplicate.star.arg"));
}
if (flags.contains(PyArgumentList.ArgFlag.IS_POS_PAST_KWD)) {
holder.registerProblem(arg, PyBundle.message("INSP.cannot.appear.past.keyword.arg"));
}
if (flags.contains(PyArgumentList.ArgFlag.IS_UNMAPPED)) {
holder.registerProblem(arg, PyBundle.message("INSP.unexpected.arg"));
}
}
}
// show unfilled params
ASTNode our_node = node.getNode();
if (our_node != null) {
ASTNode close_paren = our_node.findChildByType(PyTokenTypes.RPAR);
if (close_paren != null) {
for (PyParameter param : result.getUnmappedParams()) {
holder.registerProblem(close_paren.getPsi(), PyBundle.message("INSP.parameter.$0.unfilled", param.getName()));
}
}
}
@@ -19,6 +19,10 @@ public class PyInspectionVisitor extends PyElementVisitor {
myHolder = holder;
}
public ProblemsHolder getHolder() {
return myHolder;
}
protected final void registerProblem(final PsiElement element,
final String message){
if (element == null || element.getTextLength() == 0){
@@ -65,6 +65,9 @@ public class FunctionParsing extends Parsing {
if (myBuilder.getTokenType() == PyTokenTypes.LPAR) {
getExpressionParser().parseArgumentList(myBuilder);
}
else { // empty arglist node, so we always have it
myBuilder.mark().done(PyElementTypes.ARGUMENT_LIST);
}
checkMatches(PyTokenTypes.STATEMENT_BREAK, message("PARSE.expected.statement.break"));
decoratorMarker.done(PyElementTypes.DECORATOR_CALL);
decorated = true;
@@ -50,10 +50,6 @@ public interface PyCallExpression extends PyExpression {
enum Flag {
/**
* First arg of the call is implicit, drop first parameter.
*/
IMPLICIT_FIRST_ARG,
/**
* Called function is decorated with @classmethod, first param is the class.
*/
@@ -70,10 +66,12 @@ public interface PyCallExpression extends PyExpression {
class PyMarkedFunction {
PyFunction myFunction;
EnumSet<Flag> myFlags;
int myImplicitOffset;
public PyMarkedFunction(@NotNull PyFunction function, EnumSet<Flag> flags) {
public PyMarkedFunction(@NotNull PyFunction function, EnumSet<Flag> flags, int offset) {
myFunction = function;
myFlags = flags;
myImplicitOffset = offset;
}
public PyFunction getFunction() {
@@ -84,6 +82,15 @@ public interface PyCallExpression extends PyExpression {
return myFlags;
}
/**
* @return number of implicitly passed positional parameters; 0 means no parameters are passed implicitly.
* Note that a <tt>*args</tt> is never markeg as passed implicitly.
* E.g. for a function like <tt>foo(a, b, *args)</tt> always holds <tt>getImplicitOffset() < 2</tt>.
*/
public int getImplicitOffset() {
return myImplicitOffset;
}
}
}
@@ -42,6 +42,10 @@ public class PyElementVisitor extends PsiElementVisitor {
visitPyExpression(node);
}
public void visitPyDecoratorList(final PyDecoratorList node) {
visitElement(node);
}
public void visitPyGeneratorExpression(final PyGeneratorExpression node) {
visitPyExpression(node);
}
@@ -258,7 +258,6 @@ public class PyArgumentListImpl extends PyElementImpl implements PyArgumentList
ret.my_marked_func = resolved_callee;
if (resolved_callee != null) {
PyFunction func = resolved_callee.getFunction();
boolean implicit_self = resolved_callee.getFlags().contains(PyCallExpression.Flag.IMPLICIT_FIRST_ARG);
PyParameter[] params = func.getParameterList().getParameters();
// prepare args and slots
List<PyExpression> unmatched_args = new LinkedList<PyExpression>();
@@ -298,11 +297,11 @@ public class PyArgumentListImpl extends PyElementImpl implements PyArgumentList
}
}
}
// rule out 'self'
// rule out 'self' or other implicit params
int param_index = 0;
if (implicit_self && (params.length > 0)) {
param_slots.remove(params[0].getName()); // the self param
param_index = 1;
for (int i=0; i < resolved_callee.getImplicitOffset() && i < params.length; i+=1) {
param_slots.remove(params[i].getName()); // the self param
param_index += 1;
}
boolean seen_tuple_arg = false;
boolean seen_kwd_arg = false;
@@ -64,11 +64,12 @@ public class PyCallExpressionHelper {
else resolved = cref.resolve();
if (resolved != null) {
EnumSet<PyCallExpression.Flag> flags = EnumSet.noneOf(PyCallExpression.Flag.class);
int implicit_offset = 0;
//boolean is_inst = isByInstance();
if (isByInstance(us)) flags.add(PyCallExpression.Flag.IMPLICIT_FIRST_ARG);
if (isByInstance(us)) implicit_offset += 1;
if (resolved instanceof PyFunction) {
PyFunction meth = (PyFunction)resolved; // constructor call?
if (PyNames.INIT.equals(meth.getName())) flags.add(PyCallExpression.Flag.IMPLICIT_FIRST_ARG);
if (PyNames.INIT.equals(meth.getName())) implicit_offset += 1;
// look for closest decorator
PyDecoratorList decolist = meth.getDecoratorList();
if (decolist != null) {
@@ -80,7 +81,7 @@ public class PyCallExpressionHelper {
if (deco.isBuiltin()) {
if (PyNames.STATICMETHOD.equals(deconame)) {
flags.add(PyCallExpression.Flag.STATICMETHOD);
flags.remove(PyCallExpression.Flag.IMPLICIT_FIRST_ARG);
if (implicit_offset > 0) implicit_offset -= 1; // might have marked it as implicit 'self'
}
else if (PyNames.CLASSMETHOD.equals(deconame)) {
flags.add(PyCallExpression.Flag.CLASSMETHOD);
@@ -91,7 +92,7 @@ public class PyCallExpressionHelper {
}
}
if (!(resolved instanceof PyFunction)) return null; // omg, bogus __init__
return new PyCallExpression.PyMarkedFunction((PyFunction) resolved, flags);
return new PyCallExpression.PyMarkedFunction((PyFunction)resolved, flags, implicit_offset);
}
}
}
@@ -72,7 +72,8 @@ public class PyDecoratorImpl extends PyPresentableElementImpl<PyDecoratorStub> i
}
public boolean hasArgumentList() {
return getNode().findChildByType(PyElementTypes.ARGUMENT_LIST) != null;
ASTNode arglist_node = getNode().findChildByType(PyElementTypes.ARGUMENT_LIST);
return (arglist_node != null) && (arglist_node.findChildByType(PyTokenTypes.LPAR) != null);
}
public PyExpression getCallee() {
@@ -100,7 +101,7 @@ public class PyDecoratorImpl extends PyPresentableElementImpl<PyDecoratorStub> i
PyMarkedFunction callee = PyCallExpressionHelper.resolveCallee(this);
if (callee == null) return null;
if (! hasArgumentList()) {
callee.getFlags().add(Flag.IMPLICIT_FIRST_ARG); // NOTE: assumes mutability
callee = new PyMarkedFunction(callee.getFunction(), callee.getFlags(), callee.getImplicitOffset() + 1);
}
return callee;
}
@@ -5,6 +5,7 @@ import com.jetbrains.python.PyElementTypes;
import com.jetbrains.python.psi.PyDecoratorList;
import com.jetbrains.python.psi.stubs.PyDecoratorListStub;
import com.jetbrains.python.psi.PyDecorator;
import com.jetbrains.python.psi.PyElementVisitor;
import org.jetbrains.annotations.NotNull;
/**
@@ -18,6 +19,11 @@ public class PyDecoratorListImpl extends PyBaseElementImpl<PyDecoratorListStub>
super(astNode);
}
@Override
protected void acceptPyVisitor(PyElementVisitor pyVisitor) {
pyVisitor.visitPyDecoratorList(this);
}
public PyDecoratorListImpl(final PyDecoratorListStub stub) {
super(stub, PyElementTypes.DECORATOR_LIST);
}
@@ -80,4 +80,44 @@
<line>66</line>
<description>Unexpected argument</description>
</problem>
<problem>
<file>badarglist.py</file>
<line>80</line>
<description>Parameter 'param' unfilled</description>
</problem>
<problem>
<file>badarglist.py</file>
<line>84</line>
<description>Unexpected argument</description>
</problem>
<problem>
<file>badarglist.py</file>
<line>90</line>
<description>Parameter 'p2' unfilled</description>
</problem>
<problem>
<file>badarglist.py</file>
<line>94</line>
<description>Parameter 'p2' unfilled</description>
</problem>
<problem>
<file>badarglist.py</file>
<line>119</line>
<description>Parameter 'param' unfilled</description>
</problem>
<problem>
<file>badarglist.py</file>
<line>128</line>
<description>Parameter 'p1' unfilled</description>
</problem>
<problem>
<file>badarglist.py</file>
<line>128</line>
<description>Parameter 'p2' unfilled</description>
</problem>
<problem>
<file>badarglist.py</file>
<line>132</line>
<description>Parameter 'p2' unfilled</description>
</problem>
</problems>
@@ -5,6 +5,8 @@ PyFile:DecoratedFunction.py
PsiElement(Py:AT)('@')
PyReferenceExpression: staticmethod
PsiElement(Py:IDENTIFIER)('staticmethod')
PyArgumentList
<empty list>
PsiWhiteSpace('\n')
PyDecorator: @xmlize
PsiElement(Py:AT)('@')
@@ -24,7 +24,7 @@ public class PyResolveCalleeTest extends ResolveTestCase {
public void testInstanceCall() throws Exception {
PyCallExpression.PyMarkedFunction resolved = resolveCallee();
assertNotNull(resolved.getFunction());
assertTrue(resolved.getFlags().equals(EnumSet.of(PyCallExpression.Flag.IMPLICIT_FIRST_ARG)));
assertEquals(1, resolved.getImplicitOffset());
}
public void testClassCall() throws Exception {
@@ -36,7 +36,7 @@ public class PyResolveCalleeTest extends ResolveTestCase {
public void testDecoCall() throws Exception {
PyCallExpression.PyMarkedFunction resolved = resolveCallee();
assertNotNull(resolved.getFunction());
assertTrue(resolved.getFlags().equals(EnumSet.of(PyCallExpression.Flag.IMPLICIT_FIRST_ARG)));
assertEquals(1, resolved.getImplicitOffset());
}
public void testDecoParamCall() throws Exception {