diff --git a/python/ql/src/Expressions/CallArgs.qll b/python/ql/src/Expressions/CallArgs.qll index 6d9baf6223d..61ce5c7dc8c 100644 --- a/python/ql/src/Expressions/CallArgs.qll +++ b/python/ql/src/Expressions/CallArgs.qll @@ -154,14 +154,17 @@ predicate too_few_args(Call call, Value callable, int limit) { not exists(call.getKwargs()) and arg_count(call) < limit and exists(FunctionValue func | func = get_function_or_initializer(callable) | - call = func.getACall().getNode() and + call = func.getAFunctionCall().getNode() and limit = func.minParameters() and - // The combination of misuse of `mox.Mox().StubOutWithMock()` - // and a bug in mox's implementation of methods results in having to - // pass 1 too few arguments to the mocked function. + /* + * The combination of misuse of `mox.Mox().StubOutWithMock()` + * and a bug in mox's implementation of methods results in having to + * pass 1 too few arguments to the mocked function. + */ + not (useOfMoxInModule(call.getEnclosingModule()) and func.isNormalMethod()) or - call = func.getACall().getNode() and limit = func.minParameters() - 1 + call = func.getAMethodCall().getNode() and limit = func.minParameters() - 1 or callable instanceof ClassValue and call.getAFlowNode() = get_a_call(callable) and @@ -198,9 +201,9 @@ predicate too_many_args(Call call, Value callable, int limit) { not func.getScope().hasVarArg() and limit >= 0 | - call = func.getACall().getNode() and limit = func.maxParameters() + call = func.getAFunctionCall().getNode() and limit = func.maxParameters() or - call = func.getACall().getNode() and limit = func.maxParameters() - 1 + call = func.getAMethodCall().getNode() and limit = func.maxParameters() - 1 or callable instanceof ClassValue and call.getAFlowNode() = get_a_call(callable) and @@ -254,3 +257,8 @@ predicate overridden_call(FunctionValue func, FunctionValue overriding, Call cal overriding.overrides(func) and overriding.getACall().getNode() = call } + +/** Holds if `func` will raise a `NotImplemented` error. */ +predicate isAbstract(FunctionValue func) { + func.getARaisedType() = ClassValue::notImplementedError() +} diff --git a/python/ql/src/Expressions/WrongNumberArgumentsInCall.ql b/python/ql/src/Expressions/WrongNumberArgumentsInCall.ql index 732cb7a8b7e..02bc685c096 100644 --- a/python/ql/src/Expressions/WrongNumberArgumentsInCall.ql +++ b/python/ql/src/Expressions/WrongNumberArgumentsInCall.ql @@ -14,22 +14,17 @@ import python import CallArgs -from Call call, FunctionObject func, string too, string should, int limit +from Call call, FunctionValue func, string too, string should, int limit where - ( - too_many_args_objectapi(call, func, limit) and - too = "too many arguments" and - should = "no more than " - or - too_few_args_objectapi(call, func, limit) and - too = "too few arguments" and - should = "no fewer than " - ) and - not func.isAbstract() and - not exists(FunctionObject overridden | - func.overrides(overridden) and correct_args_if_called_as_method_objectapi(call, overridden) - ) and - /* The semantics of `__new__` can be a bit subtle, so we simply exclude `__new__` methods */ - not func.getName() = "__new__" -select call, "Call to $@ with " + too + "; should be " + should + limit.toString() + ".", func, - func.descriptiveString() +( + too_many_args(call, func, limit) and too = "too many arguments" and should = "no more than " + or + too_few_args(call, func, limit) and too = "too few arguments" and should = "no fewer than " +) and +not isAbstract(func) and +not exists(FunctionValue overridden | func.overrides(overridden) and correct_args_if_called_as_method(call, overridden)) +/* The semantics of `__new__` can be a bit subtle, so we simply exclude `__new__` methods */ +and not func.getName() = "__new__" + +select call, "Call to $@ with " + too + "; should be " + should + limit.toString() + ".", func, func.descriptiveString() + diff --git a/python/ql/src/semmle/python/objects/ObjectAPI.qll b/python/ql/src/semmle/python/objects/ObjectAPI.qll index 2a5bf0a0954..65b3326e602 100644 --- a/python/ql/src/semmle/python/objects/ObjectAPI.qll +++ b/python/ql/src/semmle/python/objects/ObjectAPI.qll @@ -608,6 +608,12 @@ abstract class FunctionValue extends CallableValue { ) } + /** Gets a class that may be raised by this function */ + abstract ClassValue getARaisedType(); + + /** Gets a call-site from where this function is called as a function */ + CallNode getAFunctionCall() { result.getFunction().pointsTo() = this } + /** Gets a call-site from where this function is called as a method */ CallNode getAMethodCall() { exists(BoundMethodObjectInternal bm | @@ -656,6 +662,8 @@ class PythonFunctionValue extends FunctionValue { /** Gets a control flow node corresponding to a return statement in this function */ ControlFlowNode getAReturnedNode() { result = this.getScope().getAReturnValueFlowNode() } + override ClassValue getARaisedType() { scope_raises(result, this.getScope()) } + override ClassValue getAnInferredReturnType() { /* We have to do a special version of this because builtin functions have no * explicit return nodes that we can query and get the class of. @@ -676,6 +684,11 @@ class BuiltinFunctionValue extends FunctionValue { override int maxParameters() { none() } + override ClassValue getARaisedType() { + /* Information is unavailable for C code in general */ + none() + } + override ClassValue getAnInferredReturnType() { /* We have to do a special version of this because builtin functions have no * explicit return nodes that we can query and get the class of. @@ -702,6 +715,11 @@ class BuiltinMethodValue extends FunctionValue { override int maxParameters() { none() } + override ClassValue getARaisedType() { + /* Information is unavailable for C code in general */ + none() + } + override ClassValue getAnInferredReturnType() { result = TBuiltinClassObject(this.(BuiltinMethodObjectInternal).getReturnType()) } @@ -929,6 +947,9 @@ module ClassValue { /** Get the `ClassValue` for the `LookupError` class. */ ClassValue lookupError() { result = TBuiltinClassObject(Builtin::builtin("LookupError")) } + /** Get the `ClassValue` for the `IndexError` class. */ + ClassValue indexError() { result = TBuiltinClassObject(Builtin::builtin("IndexError")) } + /** Get the `ClassValue` for the `IOError` class. */ ClassValue ioError() { result = TBuiltinClassObject(Builtin::builtin("IOError")) } @@ -949,4 +970,7 @@ module ClassValue { ClassValue unicodeDecodeError() { result = TBuiltinClassObject(Builtin::builtin("UnicodeDecodeError")) } + + /** Get the `ClassValue` for the `SystemExit` class. */ + ClassValue systemExit() { result = TBuiltinClassObject(Builtin::builtin("SystemExit")) } } diff --git a/python/ql/src/semmle/python/types/Exceptions.qll b/python/ql/src/semmle/python/types/Exceptions.qll index c6aa76a1919..7fe1b274664 100644 --- a/python/ql/src/semmle/python/types/Exceptions.qll +++ b/python/ql/src/semmle/python/types/Exceptions.qll @@ -36,19 +36,36 @@ class RaisingNode extends ControlFlowNode { * Gets the type of an exception that may be raised * at this control flow node */ - ClassObject getARaisedType() { - result = this.localRaisedType() + ClassObject getARaisedType_objectapi() { + result = this.localRaisedType_objectapi() or exists(FunctionObject func | this = func.getACall() | result = func.getARaisedType()) or + result = systemExitRaise_objectapi() + } + + /** + * Gets the type of an exception that may be raised + * at this control flow node + */ + ClassValue getARaisedType() { + result = this.localRaisedType() + or + exists(FunctionValue func | this = func.getACall() | result = func.getARaisedType()) + or result = systemExitRaise() } pragma[noinline] - private ClassObject systemExitRaise() { this.quits() and result = Object::builtin("SystemExit") } + private ClassObject systemExitRaise_objectapi() { + this.quits() and result = Object::builtin("SystemExit") + } + + pragma[noinline] + private ClassValue systemExitRaise() { this.quits() and result = ClassValue::systemExit() } pragma[noinline, nomagic] - private ClassObject localRaisedType() { + private ClassObject localRaisedType_objectapi() { result.isSubclassOf(theBaseExceptionType()) and ( exists(ControlFlowNode ex | @@ -62,8 +79,8 @@ class RaisingNode extends ControlFlowNode { or exists(ExceptFlowNode except | except = this.getAnExceptionalSuccessor() and - except.handles(result) and - result = this.innateException() + except.handles_objectapi(result) and + result = this.innateException_objectapi() ) or not exists(ExceptFlowNode except | except = this.getAnExceptionalSuccessor()) and @@ -74,8 +91,36 @@ class RaisingNode extends ControlFlowNode { ) } + pragma[noinline, nomagic] + private ClassValue localRaisedType() { + result.getASuperType() = ClassValue::baseException() and + ( + exists(ControlFlowNode ex | + ex = this.getExceptionNode() and + (ex.pointsTo(result) or ex.pointsTo().getClass() = result) + ) + or + this.getNode() instanceof ImportExpr and result = ClassValue::importError() + or + this.getNode() instanceof Print and result = ClassValue::ioError() + or + exists(ExceptFlowNode except | + except = this.getAnExceptionalSuccessor() and + except.handles(result) and + result = this.innateException() + ) + or + not exists(ExceptFlowNode except | except = this.getAnExceptionalSuccessor()) and + sequence_or_mapping(this) and + result = ClassValue::lookupError() + or + this.read_write_call() and result = ClassValue::ioError() + ) + } + + /** Holds if this is an innate exception (AttributeError, NameError, IndexError, or KeyError). */ pragma[noinline] - ClassObject innateException() { + ClassObject innateException_objectapi() { this.getNode() instanceof Attribute and result = theAttributeErrorType() or this.getNode() instanceof Name and result = theNameErrorType() @@ -85,6 +130,18 @@ class RaisingNode extends ControlFlowNode { this.getNode() instanceof Subscript and result = theKeyErrorType() } + /** Holds if this is an innate exception (AttributeError, NameError, IndexError, or KeyError). */ + pragma[noinline] + ClassValue innateException() { + this.getNode() instanceof Attribute and result = ClassValue::attributeError() + or + this.getNode() instanceof Name and result = ClassValue::nameError() + or + this.getNode() instanceof Subscript and result = ClassValue::indexError() + or + this.getNode() instanceof Subscript and result = ClassValue::keyError() + } + /** * Whether this control flow node raises an exception, * but the type of the exception it raises cannot be inferred. @@ -114,7 +171,7 @@ class RaisingNode extends ControlFlowNode { /** Whether (as inferred by type inference) it is highly unlikely (or impossible) for control to flow from this to succ. */ predicate unlikelySuccessor(ControlFlowNode succ) { succ = this.getAnExceptionalSuccessor() and - not this.viableExceptionEdge(succ, _) and + not this.viableExceptionEdge_objectapi(succ, _) and not this.raisesUnknownType() or exists(FunctionObject func | @@ -132,7 +189,30 @@ class RaisingNode extends ControlFlowNode { } /** Whether it is considered plausible that 'raised' can be raised across the edge this-succ */ - predicate viableExceptionEdge(ControlFlowNode succ, ClassObject raised) { + predicate viableExceptionEdge_objectapi(ControlFlowNode succ, ClassObject raised) { + raised.isLegalExceptionType() and + raised = this.getARaisedType_objectapi() and + succ = this.getAnExceptionalSuccessor() and + ( + /* An 'except' that handles raised and there is no more previous handler */ + succ.(ExceptFlowNode).handles_objectapi(raised) and + not exists(ExceptFlowNode other, StmtList s, int i, int j | + not other = succ and + other.handles_objectapi(raised) and + s.getItem(i) = succ.getNode() and + s.getItem(j) = other.getNode() + | + j < i + ) + or + /* Any successor that is not an 'except', provided that 'raised' is not handled by a different successor. */ + not this.getAnExceptionalSuccessor().(ExceptFlowNode).handles_objectapi(raised) and + not succ instanceof ExceptFlowNode + ) + } + + /** Whether it is considered plausible that 'raised' can be raised across the edge this-succ */ + predicate viableExceptionEdge(ControlFlowNode succ, ClassValue raised) { raised.isLegalExceptionType() and raised = this.getARaisedType() and succ = this.getAnExceptionalSuccessor() and @@ -159,7 +239,19 @@ class RaisingNode extends ControlFlowNode { * plausible that the scope `s` can be exited with exception `raised` * at this point. */ - predicate viableExceptionalExit(Scope s, ClassObject raised) { + predicate viableExceptionalExit_objectapi(Scope s, ClassObject raised) { + raised.isLegalExceptionType() and + raised = this.getARaisedType_objectapi() and + this.isExceptionalExit(s) and + not this.getAnExceptionalSuccessor().(ExceptFlowNode).handles_objectapi(raised) + } + + /** + * Whether this exceptional exit is viable. That is, is it + * plausible that the scope `s` can be exited with exception `raised` + * at this point. + */ + predicate viableExceptionalExit(Scope s, ClassValue raised) { raised.isLegalExceptionType() and raised = this.getARaisedType() and this.isExceptionalExit(s) and @@ -170,7 +262,29 @@ class RaisingNode extends ControlFlowNode { /** Is this a sequence or mapping subscript x[i]? */ private predicate sequence_or_mapping(RaisingNode r) { r.getNode() instanceof Subscript } -private predicate current_exception(ClassObject ex, BasicBlock b) { +private predicate current_exception_objectapi(ClassObject ex, BasicBlock b) { + exists(RaisingNode r | + r.viableExceptionEdge_objectapi(b.getNode(0), ex) and not b.getNode(0) instanceof ExceptFlowNode + ) + or + exists(BasicBlock prev | + current_exception_objectapi(ex, prev) and + exists(ControlFlowNode pred, ControlFlowNode succ | + pred = prev.getLastNode() and succ = b.getNode(0) + | + pred.getASuccessor() = succ and + ( + /* Normal control flow */ + not pred.getAnExceptionalSuccessor() = succ + or + /* Re-raise the current exception, propagating to the successor */ + pred instanceof ReraisingNode + ) + ) + ) +} + +private predicate current_exception(ClassValue ex, BasicBlock b) { exists(RaisingNode r | r.viableExceptionEdge(b.getNode(0), ex) and not b.getNode(0) instanceof ExceptFlowNode ) @@ -211,7 +325,19 @@ private predicate unknown_current_exception(BasicBlock b) { } /** INTERNAL -- Use FunctionObject.getARaisedType() instead */ -predicate scope_raises(ClassObject ex, Scope s) { +predicate scope_raises_objectapi(ClassObject ex, Scope s) { + exists(BasicBlock b | + current_exception_objectapi(ex, b) and + b.getLastNode().isExceptionalExit(s) + | + b.getLastNode() instanceof ReraisingNode + ) + or + exists(RaisingNode r | r.viableExceptionalExit_objectapi(s, ex)) +} + +/** INTERNAL -- Use FunctionObject.getARaisedType() instead */ +predicate scope_raises(ClassValue ex, Scope s) { exists(BasicBlock b | current_exception(ex, b) and b.getLastNode().isExceptionalExit(s) @@ -299,11 +425,18 @@ class ExceptFlowNode extends ControlFlowNode { } /** Whether this `except` handles `cls` */ - predicate handles(ClassObject cls) { + predicate handles_objectapi(ClassObject cls) { exists(ClassObject handled | this.handledException_objectapi(handled, _, _) | cls.getAnImproperSuperType() = handled ) } + + /** Whether this `except` handles `cls` */ + predicate handles(ClassValue cls) { + exists(ClassValue handled | this.handledException(handled, _, _) | + cls.getASuperType() = handled + ) + } } private ControlFlowNode element_from_tuple_objectapi(Object tuple) { @@ -324,7 +457,15 @@ class ReraisingNode extends RaisingNode { } /** Gets a class that may be raised by this node */ - override ClassObject getARaisedType() { + override ClassObject getARaisedType_objectapi() { + exists(BasicBlock b | + current_exception_objectapi(result, b) and + b.getNode(_) = this + ) + } + + /** Gets a class that may be raised by this node */ + override ClassValue getARaisedType() { exists(BasicBlock b | current_exception(result, b) and b.getNode(_) = this diff --git a/python/ql/src/semmle/python/types/FunctionObject.qll b/python/ql/src/semmle/python/types/FunctionObject.qll index 6c1db531184..5d3a81363db 100644 --- a/python/ql/src/semmle/python/types/FunctionObject.qll +++ b/python/ql/src/semmle/python/types/FunctionObject.qll @@ -135,7 +135,7 @@ class PyFunctionObject extends FunctionObject { /** Whether this function is a procedure, that is, it has no explicit return statement and is not a generator function */ override predicate isProcedure() { this.getFunction().isProcedure() } - override ClassObject getARaisedType() { scope_raises(result, this.getFunction()) } + override ClassObject getARaisedType() { scope_raises_objectapi(result, this.getFunction()) } override predicate raisesUnknownType() { scope_raises_unknown(this.getFunction()) } diff --git a/python/ql/test/2/library-tests/types/exceptions/ExitRaises.ql b/python/ql/test/2/library-tests/types/exceptions/ExitRaises.ql index 8e4c47a3e74..415db290a05 100644 --- a/python/ql/test/2/library-tests/types/exceptions/ExitRaises.ql +++ b/python/ql/test/2/library-tests/types/exceptions/ExitRaises.ql @@ -1,5 +1,5 @@ import python from RaisingNode r, Scope s, ClassObject cls -where r.viableExceptionalExit(s, cls) +where r.viableExceptionalExit_objectapi(s, cls) select r.getLocation().getStartLine(), r.toString(), s.toString(), cls.toString() diff --git a/python/ql/test/3/library-tests/types/exceptions/Viable.ql b/python/ql/test/3/library-tests/types/exceptions/Viable.ql index e28fa1a907c..ed388e2faf2 100644 --- a/python/ql/test/3/library-tests/types/exceptions/Viable.ql +++ b/python/ql/test/3/library-tests/types/exceptions/Viable.ql @@ -1,6 +1,6 @@ import python from RaisingNode r, ControlFlowNode n, ClassObject ex -where r.viableExceptionEdge(n, ex) +where r.viableExceptionEdge_objectapi(n, ex) select r.getLocation().getStartLine(), n.getLocation().getStartLine(), r.getNode().toString(), n.getNode().toString(), ex.toString() diff --git a/python/ql/test/library-tests/types/exceptions/ExitRaises.ql b/python/ql/test/library-tests/types/exceptions/ExitRaises.ql index 62be45dce8e..32ef268332c 100644 --- a/python/ql/test/library-tests/types/exceptions/ExitRaises.ql +++ b/python/ql/test/library-tests/types/exceptions/ExitRaises.ql @@ -1,5 +1,5 @@ import python from RaisingNode r, Scope s, ClassObject cls -where r.viableExceptionalExit(s, cls) +where r.viableExceptionalExit_objectapi(s, cls) select r.getLocation().getStartLine(), r, s.toString(), cls diff --git a/python/ql/test/library-tests/types/exceptions/Handles.ql b/python/ql/test/library-tests/types/exceptions/Handles.ql index 601f2632392..dfdf1f9d7b2 100644 --- a/python/ql/test/library-tests/types/exceptions/Handles.ql +++ b/python/ql/test/library-tests/types/exceptions/Handles.ql @@ -1,5 +1,5 @@ import python from ExceptFlowNode n, ClassObject cls -where n.handles(cls) +where n.handles_objectapi(cls) select n.getLocation().getStartLine(), cls.toString() diff --git a/python/ql/test/library-tests/types/exceptions/Viable.ql b/python/ql/test/library-tests/types/exceptions/Viable.ql index e28fa1a907c..ed388e2faf2 100644 --- a/python/ql/test/library-tests/types/exceptions/Viable.ql +++ b/python/ql/test/library-tests/types/exceptions/Viable.ql @@ -1,6 +1,6 @@ import python from RaisingNode r, ControlFlowNode n, ClassObject ex -where r.viableExceptionEdge(n, ex) +where r.viableExceptionEdge_objectapi(n, ex) select r.getLocation().getStartLine(), n.getLocation().getStartLine(), r.getNode().toString(), n.getNode().toString(), ex.toString() diff --git a/python/ql/test/query-tests/Expressions/Arguments/WrongNumberArgumentsInCall.expected b/python/ql/test/query-tests/Expressions/Arguments/WrongNumberArgumentsInCall.expected index c23418acd45..31adabb9e05 100644 --- a/python/ql/test/query-tests/Expressions/Arguments/WrongNumberArgumentsInCall.expected +++ b/python/ql/test/query-tests/Expressions/Arguments/WrongNumberArgumentsInCall.expected @@ -1,7 +1,7 @@ | use_mox.py:28:1:28:4 | f0() | Call to $@ with too few arguments; should be no fewer than 1. | use_mox.py:7:1:7:10 | Function f0 | function f0 | | use_mox.py:29:1:29:5 | f1() | Call to $@ with too few arguments; should be no fewer than 2. | use_mox.py:10:1:10:13 | Function f1 | function f1 | -| use_mox.py:32:1:32:8 | Attribute() | Call to $@ with too few arguments; should be no fewer than 1. | use_mox.py:15:5:15:20 | Function m0 | method C.m0 | -| use_mox.py:33:1:33:9 | Attribute() | Call to $@ with too few arguments; should be no fewer than 2. | use_mox.py:18:5:18:23 | Function m1 | method C.m1 | +| use_mox.py:32:1:32:8 | Attribute() | Call to $@ with too few arguments; should be no fewer than 1. | use_mox.py:15:5:15:20 | Function C.m0 | method C.m0 | +| use_mox.py:33:1:33:9 | Attribute() | Call to $@ with too few arguments; should be no fewer than 2. | use_mox.py:18:5:18:23 | Function C.m1 | method C.m1 | | wrong_arguments.py:29:1:29:4 | f0() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:3:1:3:10 | Function f0 | function f0 | | wrong_arguments.py:30:1:30:4 | f1() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:6:1:6:20 | Function f1 | function f1 | | wrong_arguments.py:31:1:31:4 | f2() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:9:1:9:14 | Function f2 | function f2 | @@ -21,5 +21,5 @@ | wrong_arguments.py:86:1:86:4 | l1() | Call to $@ with too few arguments; should be no fewer than 1. | wrong_arguments.py:71:6:71:21 | Function lambda | function lambda | | wrong_arguments.py:96:1:96:12 | f6() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:21:1:21:13 | Function f6 | function f6 | | wrong_arguments.py:97:1:97:7 | f6() | Call to $@ with too many arguments; should be no more than 2. | wrong_arguments.py:21:1:21:13 | Function f6 | function f6 | -| wrong_arguments.py:130:1:130:9 | Attribute() | Call to $@ with too few arguments; should be no fewer than 2. | wrong_arguments.py:126:5:126:31 | Function spam | method Eggs2.spam | -| wrong_arguments.py:130:1:130:9 | Attribute() | Call to $@ with too many arguments; should be no more than 0. | wrong_arguments.py:121:5:121:19 | Function spam | method Eggs1.spam | +| wrong_arguments.py:130:1:130:9 | Attribute() | Call to $@ with too few arguments; should be no fewer than 2. | wrong_arguments.py:126:5:126:31 | Function Eggs2.spam | method Eggs2.spam | +| wrong_arguments.py:130:1:130:9 | Attribute() | Call to $@ with too many arguments; should be no more than 0. | wrong_arguments.py:121:5:121:19 | Function Eggs1.spam | method Eggs1.spam | diff --git a/python/ql/test/query-tests/Functions/overriding/WrongNumberArgumentsInCall.expected b/python/ql/test/query-tests/Functions/overriding/WrongNumberArgumentsInCall.expected index 4f29401ce5b..86dd08abf16 100644 --- a/python/ql/test/query-tests/Functions/overriding/WrongNumberArgumentsInCall.expected +++ b/python/ql/test/query-tests/Functions/overriding/WrongNumberArgumentsInCall.expected @@ -1,2 +1,2 @@ -| test.py:16:9:16:21 | Attribute() | Call to $@ with too many arguments; should be no more than 0. | test.py:5:5:5:20 | Function meth1 | method Base.meth1 | -| test.py:17:9:17:20 | Attribute() | Call to $@ with too few arguments; should be no fewer than 1. | test.py:8:5:8:26 | Function meth2 | method Base.meth2 | +| test.py:16:9:16:21 | Attribute() | Call to $@ with too many arguments; should be no more than 0. | test.py:5:5:5:20 | Function Base.meth1 | method Base.meth1 | +| test.py:17:9:17:20 | Attribute() | Call to $@ with too few arguments; should be no fewer than 1. | test.py:8:5:8:26 | Function Base.meth2 | method Base.meth2 |