From 73d257e538c5fefb3dcad71ef4f69f23c6f49262 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Wed, 23 Jul 2025 14:13:58 +0100 Subject: [PATCH 01/11] Port unexpected raise away from pointsto --- .../IncorrectRaiseInSpecialMethod.ql | 152 ++++++++++++------ 1 file changed, 101 insertions(+), 51 deletions(-) diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql index 4bf52af9061f..5df5f64116e5 100644 --- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql +++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql @@ -12,16 +12,18 @@ */ import python +import semmle.python.ApiGraphs +import semmle.python.dataflow.new.internal.DataFlowDispatch -private predicate attribute_method(string name) { +private predicate attributeMethod(string name) { name = "__getattribute__" or name = "__getattr__" or name = "__setattr__" } -private predicate indexing_method(string name) { +private predicate indexingMethod(string name) { name = "__getitem__" or name = "__setitem__" or name = "__delitem__" } -private predicate arithmetic_method(string name) { +private predicate arithmeticMethod(string name) { name in [ "__add__", "__sub__", "__or__", "__xor__", "__rshift__", "__pow__", "__mul__", "__neg__", "__radd__", "__rsub__", "__rdiv__", "__rfloordiv__", "__div__", "__rdiv__", "__rlshift__", @@ -32,7 +34,7 @@ private predicate arithmetic_method(string name) { ] } -private predicate ordering_method(string name) { +private predicate orderingMethod(string name) { name = "__lt__" or name = "__le__" @@ -40,13 +42,9 @@ private predicate ordering_method(string name) { name = "__gt__" or name = "__ge__" - or - name = "__cmp__" and major_version() = 2 } -private predicate cast_method(string name) { - name = "__nonzero__" and major_version() = 2 - or +private predicate castMethod(string name) { name = "__int__" or name = "__float__" @@ -58,63 +56,115 @@ private predicate cast_method(string name) { name = "__complex__" } -predicate correct_raise(string name, ClassObject ex) { - ex.getAnImproperSuperType() = theTypeErrorType() and +predicate correctRaise(string name, Expr exec) { + execIsOfType(exec, "TypeError") and ( - name = "__copy__" or - name = "__deepcopy__" or - name = "__call__" or - indexing_method(name) or - attribute_method(name) + indexingMethod(name) or + attributeMethod(name) ) or - preferred_raise(name, ex) - or - preferred_raise(name, ex.getASuperType()) + exists(string execName | + preferredRaise(name, execName, _) and + execIsOfType(exec, execName) + ) } -predicate preferred_raise(string name, ClassObject ex) { - attribute_method(name) and ex = theAttributeErrorType() - or - indexing_method(name) and ex = Object::builtin("LookupError") - or - ordering_method(name) and ex = theTypeErrorType() - or - arithmetic_method(name) and ex = Object::builtin("ArithmeticError") - or - name = "__bool__" and ex = theTypeErrorType() +predicate preferredRaise(string name, string execName, string message) { + // TODO: execName should be an IPA type + attributeMethod(name) and + execName = "AttributeError" and + message = "should raise an AttributeError instead." + or + indexingMethod(name) and + execName = "LookupError" and + message = "should raise a LookupError (KeyError or IndexError) instead." + or + orderingMethod(name) and + execName = "TypeError" and + message = "should raise a TypeError, or return NotImplemented instead." + or + arithmeticMethod(name) and + execName = "ArithmeticError" and + message = "should raise an ArithmeticError, or return NotImplemented instead." + or + name = "__bool__" and + execName = "TypeError" and + message = "should raise a TypeError instead." } -predicate no_need_to_raise(string name, string message) { - name = "__hash__" and message = "use __hash__ = None instead" - or - cast_method(name) and message = "there is no need to implement the method at all." +predicate execIsOfType(Expr exec, string execName) { + exists(string subclass | + execName = "TypeError" and + subclass = "TypeError" + or + execName = "LookupError" and + subclass = ["LookupError", "KeyError", "IndexError"] + or + execName = "ArithmeticError" and + subclass = ["ArithmeticError", "FloatingPointError", "OverflowError", "ZeroDivisionError"] + or + execName = "AttributeError" and + subclass = "AttributeError" + | + exec = API::builtin(subclass).getACall().asExpr() + or + exec = API::builtin(subclass).getASubclass().getACall().asExpr() + ) } -predicate is_abstract(FunctionObject func) { - func.getFunction().getADecorator().(Name).getId().matches("%abstract%") +predicate noNeedToAlwaysRaise(Function meth, string message, boolean allowNotImplemented) { + meth.getName() = "__hash__" and + message = "use __hash__ = None instead." and + allowNotImplemented = false + or + castMethod(meth.getName()) and + message = "this method does not need to be implemented." and + allowNotImplemented = true and + not exists(Function overridden | + overridden.getName() = meth.getName() and + overridden.getScope() = getADirectSuperclass+(meth.getScope()) and + alwaysRaises(overridden, _) + ) } -predicate always_raises(FunctionObject f, ClassObject ex) { - ex = f.getARaisedType() and - strictcount(f.getARaisedType()) = 1 and - not exists(f.getFunction().getANormalExit()) and - /* raising StopIteration is equivalent to a return in a generator */ - not ex = theStopIterationType() +predicate isAbstract(Function func) { func.getADecorator().(Name).getId().matches("%abstract%") } + +predicate alwaysRaises(Function f, Expr exec) { + directlyRaises(f, exec) and + strictcount(Expr e | directlyRaises(f, e)) = 1 and + not exists(f.getANormalExit()) } -from FunctionObject f, ClassObject cls, string message +predicate directlyRaises(Function f, Expr exec) { + exists(Raise r | + r.getScope() = f and + exec = r.getException() and + not exec = API::builtin("StopIteration").asSource().asExpr() + ) +} + +predicate isNotImplementedError(Expr exec) { + exec = API::builtin("NotImplementedError").getACall().asExpr() +} + +from Function f, Expr exec, string message where - f.getFunction().isSpecialMethod() and - not is_abstract(f) and - always_raises(f, cls) and + f.isSpecialMethod() and + not isAbstract(f) and + directlyRaises(f, exec) and ( - no_need_to_raise(f.getName(), message) and not cls.getName() = "NotImplementedError" + exists(boolean allowNotImplemented, string subMessage | + alwaysRaises(f, exec) and + noNeedToAlwaysRaise(f, subMessage, allowNotImplemented) and + (allowNotImplemented = false or not isNotImplementedError(exec)) and + message = "This method always raises $@ - " + subMessage + ) or - not correct_raise(f.getName(), cls) and - not cls.getName() = "NotImplementedError" and - exists(ClassObject preferred | preferred_raise(f.getName(), preferred) | - message = "raise " + preferred.getName() + " instead" + alwaysRaises(f, exec) and // for now consider only alwaysRaises cases as original query + not isNotImplementedError(exec) and + not correctRaise(f.getName(), exec) and + exists(string subMessage | preferredRaise(f.getName(), _, subMessage) | + message = "This method always raises $@ - " + subMessage ) ) -select f, "Function always raises $@; " + message, cls, cls.toString() +select f, message, exec, exec.toString() // TODO: remove tostring From b9738066de1d9d67f73b452a16c3aea22e3a0470 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Thu, 24 Jul 2025 11:18:28 +0100 Subject: [PATCH 02/11] try excluding set methods, add methods, update alert messages --- .../IncorrectRaiseInSpecialMethod.ql | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql index 5df5f64116e5..0c61b0cf7754 100644 --- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql +++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql @@ -16,15 +16,16 @@ import semmle.python.ApiGraphs import semmle.python.dataflow.new.internal.DataFlowDispatch private predicate attributeMethod(string name) { - name = "__getattribute__" or name = "__getattr__" or name = "__setattr__" + name = ["__getattribute__", "__getattr__"] // __setattr__ excluded as it makes sense to raise different kinds of errors based on the `value` parameter } private predicate indexingMethod(string name) { - name = "__getitem__" or name = "__setitem__" or name = "__delitem__" + name = ["__getitem__", "__delitem__"] // __setitem__ excluded as it makes sense to raise different kinds of errors based on the `value` parameter } private predicate arithmeticMethod(string name) { - name in [ + name = + [ "__add__", "__sub__", "__or__", "__xor__", "__rshift__", "__pow__", "__mul__", "__neg__", "__radd__", "__rsub__", "__rdiv__", "__rfloordiv__", "__div__", "__rdiv__", "__rlshift__", "__rand__", "__ror__", "__rxor__", "__rrshift__", "__rpow__", "__rmul__", "__truediv__", @@ -35,32 +36,32 @@ private predicate arithmeticMethod(string name) { } private predicate orderingMethod(string name) { - name = "__lt__" - or - name = "__le__" - or - name = "__gt__" - or - name = "__ge__" + name = + [ + "__lt__", + "__le__", + "__gt__", + "__ge__", + ] } private predicate castMethod(string name) { - name = "__int__" - or - name = "__float__" - or - name = "__long__" - or - name = "__trunc__" - or - name = "__complex__" + name = + [ + "__int__", + "__float__", + "__long__", + "__trunc__", + "__complex__" + ] } predicate correctRaise(string name, Expr exec) { execIsOfType(exec, "TypeError") and ( indexingMethod(name) or - attributeMethod(name) + attributeMethod(name) or + name = ["__add__", "__iadd__", "__radd__"] ) or exists(string execName | @@ -81,11 +82,11 @@ predicate preferredRaise(string name, string execName, string message) { or orderingMethod(name) and execName = "TypeError" and - message = "should raise a TypeError, or return NotImplemented instead." + message = "should raise a TypeError or return NotImplemented instead." or arithmeticMethod(name) and execName = "ArithmeticError" and - message = "should raise an ArithmeticError, or return NotImplemented instead." + message = "should raise an ArithmeticError or return NotImplemented instead." or name = "__bool__" and execName = "TypeError" and @@ -120,6 +121,7 @@ predicate noNeedToAlwaysRaise(Function meth, string message, boolean allowNotImp castMethod(meth.getName()) and message = "this method does not need to be implemented." and allowNotImplemented = true and + // Allow an always raising cast method if it's overriding other behavior not exists(Function overridden | overridden.getName() = meth.getName() and overridden.getScope() = getADirectSuperclass+(meth.getScope()) and @@ -139,7 +141,7 @@ predicate directlyRaises(Function f, Expr exec) { exists(Raise r | r.getScope() = f and exec = r.getException() and - not exec = API::builtin("StopIteration").asSource().asExpr() + exec instanceof Call ) } @@ -156,15 +158,16 @@ where exists(boolean allowNotImplemented, string subMessage | alwaysRaises(f, exec) and noNeedToAlwaysRaise(f, subMessage, allowNotImplemented) and - (allowNotImplemented = false or not isNotImplementedError(exec)) and + (allowNotImplemented = true implies not isNotImplementedError(exec)) and // don't alert if it's a NotImplementedError and that's ok message = "This method always raises $@ - " + subMessage ) or - alwaysRaises(f, exec) and // for now consider only alwaysRaises cases as original query not isNotImplementedError(exec) and not correctRaise(f.getName(), exec) and exists(string subMessage | preferredRaise(f.getName(), _, subMessage) | - message = "This method always raises $@ - " + subMessage + if alwaysRaises(f, exec) + then message = "This method always raises $@ - " + subMessage + else message = "This method raises $@ - " + subMessage ) ) select f, message, exec, exec.toString() // TODO: remove tostring From b9f6657adedbbc121ad63f32ee8ce5b1133a0aa1 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Thu, 24 Jul 2025 13:50:27 +0100 Subject: [PATCH 03/11] Remove use of toString. This does also reduce reaults from cases where the exception is not a simple identifier. --- python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql index 0c61b0cf7754..ca1996a1e016 100644 --- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql +++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql @@ -71,7 +71,6 @@ predicate correctRaise(string name, Expr exec) { } predicate preferredRaise(string name, string execName, string message) { - // TODO: execName should be an IPA type attributeMethod(name) and execName = "AttributeError" and message = "should raise an AttributeError instead." @@ -94,6 +93,7 @@ predicate preferredRaise(string name, string execName, string message) { } predicate execIsOfType(Expr exec, string execName) { + // Might make sense to have execName be an IPA type here. Or part of a more general API modelling builtin/stdlib subclass relations. exists(string subclass | execName = "TypeError" and subclass = "TypeError" @@ -149,6 +149,8 @@ predicate isNotImplementedError(Expr exec) { exec = API::builtin("NotImplementedError").getACall().asExpr() } +string getExecName(Expr exec) { result = exec.(Call).getFunc().(Name).getId() } + from Function f, Expr exec, string message where f.isSpecialMethod() and @@ -170,4 +172,4 @@ where else message = "This method raises $@ - " + subMessage ) ) -select f, message, exec, exec.toString() // TODO: remove tostring +select f, message, exec, getExecName(exec) From 362bfba0496e494c85177eb8771cccbc1ac12c58 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Thu, 24 Jul 2025 14:50:36 +0100 Subject: [PATCH 04/11] Update unit tests --- .../IncorrectRaiseInSpecialMethod.ql | 9 +-- .../IncorrectRaiseInSpecialMethod.expected | 6 ++ .../IncorrectRaiseInSpecialMethod.qlref | 2 + .../IncorrectRaiseInSpcialMethod/test.py | 66 +++++++++++++++++++ .../IncorrectRaiseInSpecialMethod.expected | 3 - .../IncorrectRaiseInSpecialMethod.qlref | 1 - 6 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.expected create mode 100644 python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.qlref create mode 100644 python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/test.py delete mode 100644 python/ql/test/query-tests/Functions/general/IncorrectRaiseInSpecialMethod.expected delete mode 100644 python/ql/test/query-tests/Functions/general/IncorrectRaiseInSpecialMethod.qlref diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql index ca1996a1e016..12107821aa66 100644 --- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql +++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql @@ -7,7 +7,7 @@ * error-handling * @problem.severity recommendation * @sub-severity high - * @precision very-high + * @precision high * @id py/unexpected-raise-in-special-method */ @@ -16,7 +16,7 @@ import semmle.python.ApiGraphs import semmle.python.dataflow.new.internal.DataFlowDispatch private predicate attributeMethod(string name) { - name = ["__getattribute__", "__getattr__"] // __setattr__ excluded as it makes sense to raise different kinds of errors based on the `value` parameter + name = ["__getattribute__", "__getattr__", "__delattr__"] // __setattr__ excluded as it makes sense to raise different kinds of errors based on the `value` parameter } private predicate indexingMethod(string name) { @@ -50,7 +50,7 @@ private predicate castMethod(string name) { [ "__int__", "__float__", - "__long__", + "__index__", "__trunc__", "__complex__" ] @@ -61,6 +61,7 @@ predicate correctRaise(string name, Expr exec) { ( indexingMethod(name) or attributeMethod(name) or + // Allow add methods to raise a TypeError, as they can be used for sequence concatenation as well as arithmetic name = ["__add__", "__iadd__", "__radd__"] ) or @@ -125,7 +126,7 @@ predicate noNeedToAlwaysRaise(Function meth, string message, boolean allowNotImp not exists(Function overridden | overridden.getName() = meth.getName() and overridden.getScope() = getADirectSuperclass+(meth.getScope()) and - alwaysRaises(overridden, _) + not alwaysRaises(overridden, _) ) } diff --git a/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.expected b/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.expected new file mode 100644 index 000000000000..3907a725ee18 --- /dev/null +++ b/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.expected @@ -0,0 +1,6 @@ +| test.py:6:5:6:33 | Function __getitem__ | This method always raises $@ - should raise a LookupError (KeyError or IndexError) instead. | test.py:7:15:7:33 | ZeroDivisionError() | ZeroDivisionError | +| test.py:9:5:9:32 | Function __getattr__ | This method always raises $@ - should raise an AttributeError instead. | test.py:10:15:10:33 | ZeroDivisionError() | ZeroDivisionError | +| test.py:12:5:12:23 | Function __bool__ | This method always raises $@ - should raise a TypeError instead. | test.py:13:15:13:26 | ValueError() | ValueError | +| test.py:15:5:15:22 | Function __int__ | This method always raises $@ - this method does not need to be implemented. | test.py:16:15:16:26 | ValueError() | ValueError | +| test.py:24:5:24:23 | Function __hash__ | This method always raises $@ - use __hash__ = None instead. | test.py:25:15:25:35 | NotImplementedError() | NotImplementedError | +| test.py:28:5:28:29 | Function __sub__ | This method raises $@ - should raise an ArithmeticError or return NotImplemented instead. | test.py:30:19:30:29 | TypeError() | TypeError | diff --git a/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.qlref b/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.qlref new file mode 100644 index 000000000000..a81e499ea66b --- /dev/null +++ b/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.qlref @@ -0,0 +1,2 @@ +query: Functions/IncorrectRaiseInSpecialMethod.ql +postprocess: utils/test/InlineExpectationsTestQuery.ql \ No newline at end of file diff --git a/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/test.py b/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/test.py new file mode 100644 index 000000000000..d5b1bc585f62 --- /dev/null +++ b/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/test.py @@ -0,0 +1,66 @@ +class A: + + def __add__(self, other): # No alert - Always allow NotImplementedError + raise NotImplementedError() + + def __getitem__(self, index): # $ Alert + raise ZeroDivisionError() + + def __getattr__(self, name): # $ Alert + raise ZeroDivisionError() + + def __bool__(self): # $ Alert + raise ValueError() + + def __int__(self): # $ Alert # Cast method need not be defined to always raise + raise ValueError() + + def __float__(self): # No alert - OK to raise conditionally + if self.z: + return 0 + else: + raise ValueError() + + def __hash__(self): # $ Alert # should use __hash__=None rather than stub implementation to make class unhashable + raise NotImplementedError() + +class B: + def __sub__(self, other): # $ Alert # should return NotImplemented instead + if not isinstance(other,B): + raise TypeError() + return self + + def __add__(self, other): # No alert - allow add to raise a TypeError, as it is sometimes used for sequence concatenation as well as arithmetic + if not isinstance(other,B): + raise TypeError() + return self + + def __setitem__(self, key, val): # No alert - allow setitem to raise arbitrary exceptions as they could be due to the value, rather than a LookupError relating to the key + if val < 0: + raise ValueError() + + def __getitem__(self, key): # No alert - indexing method allowed to raise TypeError or subclasses of LookupError. + if not isinstance(key, int): + raise TypeError() + if key < 0: + raise KeyError() + return 3 + + def __getattribute__(self, name): + if name != "a": + raise AttributeError() + return 2 + + def __div__(self, other): + if other == 0: + raise ZeroDivisionError() + return self + + +class D: + def __int__(self): + return 2 + +class E(D): + def __int__(self): # No alert - cast method may override to raise exception + raise TypeError() \ No newline at end of file diff --git a/python/ql/test/query-tests/Functions/general/IncorrectRaiseInSpecialMethod.expected b/python/ql/test/query-tests/Functions/general/IncorrectRaiseInSpecialMethod.expected deleted file mode 100644 index dd4429de02e9..000000000000 --- a/python/ql/test/query-tests/Functions/general/IncorrectRaiseInSpecialMethod.expected +++ /dev/null @@ -1,3 +0,0 @@ -| protocols.py:98:5:98:33 | Function __getitem__ | Function always raises $@; raise LookupError instead | file://:Compiled Code:0:0:0:0 | builtin-class ZeroDivisionError | builtin-class ZeroDivisionError | -| protocols.py:101:5:101:26 | Function __getattr__ | Function always raises $@; raise AttributeError instead | file://:Compiled Code:0:0:0:0 | builtin-class ZeroDivisionError | builtin-class ZeroDivisionError | -| protocols.py:104:5:104:23 | Function __bool__ | Function always raises $@; raise TypeError instead | file://:Compiled Code:0:0:0:0 | builtin-class ZeroDivisionError | builtin-class ZeroDivisionError | diff --git a/python/ql/test/query-tests/Functions/general/IncorrectRaiseInSpecialMethod.qlref b/python/ql/test/query-tests/Functions/general/IncorrectRaiseInSpecialMethod.qlref deleted file mode 100644 index 07fd22a93767..000000000000 --- a/python/ql/test/query-tests/Functions/general/IncorrectRaiseInSpecialMethod.qlref +++ /dev/null @@ -1 +0,0 @@ -Functions/IncorrectRaiseInSpecialMethod.ql \ No newline at end of file From 871688f02617921452a77f50aba33fd8c5b4dbe5 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Thu, 24 Jul 2025 16:01:57 +0100 Subject: [PATCH 05/11] Update docs --- .../IncorrectRaiseInSpecialMethod.py | 16 -------- .../IncorrectRaiseInSpecialMethod.qhelp | 40 +++++++++---------- .../IncorrectRaiseInSpecialMethod2.py | 15 ------- .../IncorrectRaiseInSpecialMethod3.py | 27 ------------- .../examples/IncorrectRaiseInSpecialMethod.py | 22 ++++++++++ .../IncorrectRaiseInSpecialMethod2.py | 7 ++++ .../IncorrectRaiseInSpecialMethod3.py | 4 ++ 7 files changed, 52 insertions(+), 79 deletions(-) delete mode 100644 python/ql/src/Functions/IncorrectRaiseInSpecialMethod.py delete mode 100644 python/ql/src/Functions/IncorrectRaiseInSpecialMethod2.py delete mode 100644 python/ql/src/Functions/IncorrectRaiseInSpecialMethod3.py create mode 100644 python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod.py create mode 100644 python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod2.py create mode 100644 python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod3.py diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.py b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.py deleted file mode 100644 index e76c27145dbb..000000000000 --- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.py +++ /dev/null @@ -1,16 +0,0 @@ -#Incorrect unhashable class -class MyMutableThing(object): - - def __init__(self): - pass - - def __hash__(self): - raise NotImplementedError("%r is unhashable" % self) - -#Make class unhashable in the standard way -class MyCorrectMutableThing(object): - - def __init__(self): - pass - - __hash__ = None diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp index f4f0cd6920ab..a0c3463b9d17 100644 --- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp +++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp @@ -9,7 +9,7 @@ When the expression a + b is evaluated the Python virtual machine w is not implemented it will call type(b).__radd__(b, a).

Since the virtual machine calls these special methods for common expressions, users of the class will expect these operations to raise standard exceptions. -For example, users would expect that the expression a.b might raise an AttributeError +For example, users would expect that the expression a.b may raise an AttributeError if the object a does not have an attribute b. If a KeyError were raised instead, then this would be unexpected and may break code that expected an AttributeError, but not a KeyError. @@ -20,18 +20,18 @@ Therefore, if a method is unable to perform the expected operation then its resp

    -
  • Attribute access, a.b: Raise AttributeError
  • -
  • Arithmetic operations, a + b: Do not raise an exception, return NotImplemented instead.
  • -
  • Indexing, a[b]: Raise KeyError.
  • -
  • Hashing, hash(a): Use __hash__ = None to indicate that an object is unhashable.
  • -
  • Equality methods, a != b: Never raise an exception, always return True or False.
  • -
  • Ordering comparison methods, a < b: Raise a TypeError if the objects cannot be ordered.
  • +
  • Attribute access, a.b (__getattr__): Raise AttributeError
  • +
  • Arithmetic operations, a + b (__add__): Do not raise an exception, return NotImplemented instead.
  • +
  • Indexing, a[b] (__getitem__): Raise KeyError or IndexError.
  • +
  • Hashing, hash(a) (__hash__): Should not raise an exception. Use __hash__ = None to indicate that an object is unhashable rather than raising an exception.
  • +
  • Equality methods, a == b (__eq__): Never raise an exception, always return True or False.
  • +
  • Ordering comparison methods, a < b (__lt__): Raise a TypeError if the objects cannot be ordered.
  • Most others: Ideally, do not implement the method at all, otherwise raise TypeError to indicate that the operation is unsupported.
-

If the method is meant to be abstract, then declare it so using the @abstractmethod decorator. +

If the method is intended to be abstract, then declare it so using the @abstractmethod decorator. Otherwise, either remove the method or ensure that the method raises an exception of the correct type.

@@ -39,31 +39,29 @@ Otherwise, either remove the method or ensure that the method raises an exceptio

-This example shows two unhashable classes. The first class is unhashable in a non-standard way which may cause maintenance problems. -The second, corrected, class uses the standard idiom for unhashable classes. +In the following example, the __add__ method of A raises a TypeError if other is of the wrong type. +However, it should return NotImplemented instead of rising an exception, to allow other classes to support adding to A. +This is demonstrated in the class B.

- +

-In this example, the first class is implicitly abstract; the __add__ method is unimplemented, -presumably with the expectation that it will be implemented by sub-classes. -The second class makes this explicit with an @abstractmethod decoration on the unimplemented __add__ method. +In the following example, the __getitem__ method of C raises a ValueError, rather than a KeyError or IndexError as expected.

- +

-In this last example, the first class implements a collection backed by the file store. -However, should an IOError be raised in the __getitem__ it will propagate to the caller. -The second class handles any IOError by reraising a KeyError which is the standard exception for -the __getitem__ method. +In the following example, the class __hash__ method of D raises TypeError. +This causes D to be incorrectly identified as hashable by isinstance(obj, collections.abc.Hashable); so the correct +way to make a class unhashable is to set __hash__ = None.

- +
  • Python Language Reference: Special Method Names.
  • -
  • Python Library Reference: Exceptions.
  • +
  • Python Library Reference: Exceptions.
  • diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod2.py b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod2.py deleted file mode 100644 index 405400bfe614..000000000000 --- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod2.py +++ /dev/null @@ -1,15 +0,0 @@ - -#Abstract base class, but don't declare it. -class ImplicitAbstractClass(object): - - def __add__(self, other): - raise NotImplementedError() - -#Make abstractness explicit. -class ExplicitAbstractClass: - __metaclass__ = ABCMeta - - @abstractmethod - def __add__(self, other): - raise NotImplementedError() - diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod3.py b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod3.py deleted file mode 100644 index 048d5043b4dc..000000000000 --- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod3.py +++ /dev/null @@ -1,27 +0,0 @@ - -#Incorrect file-backed table -class FileBackedTable(object): - - def __getitem__(self, key): - if key not in self.index: - raise IOError("Key '%s' not in table" % key) - else: - #May raise an IOError - return self.backing.get_row(key) - -#Correct by transforming exception -class ObjectLikeFileBackedTable(object): - - def get_from_key(self, key): - if key not in self.index: - raise IOError("Key '%s' not in table" % key) - else: - #May raise an IOError - return self.backing.get_row(key) - - def __getitem__(self, key): - try: - return self.get_from_key(key) - except IOError: - raise KeyError(key) - diff --git a/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod.py b/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod.py new file mode 100644 index 000000000000..77c623bef794 --- /dev/null +++ b/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod.py @@ -0,0 +1,22 @@ +class A: + def __init__(self, a): + self.a = a + + def __add__(self, other): + # BAD: Should return NotImplemented instead of raising + if not isinstance(other,A): + raise TypeError(f"Cannot add A to {other.__type__}") + return A(self.a + other.a) + +class B: + def __init__(self, a): + self.a = a + + def __add__(self, other): + # GOOD: Returning NotImplemented allows for other classes to support adding do B. + if not isinstance(other,B): + return NotImplemented + return B(self.a + other.a) + + + diff --git a/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod2.py b/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod2.py new file mode 100644 index 000000000000..ba5f90f46708 --- /dev/null +++ b/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod2.py @@ -0,0 +1,7 @@ +class C: + def __getitem__(self, idx): + if self.idx < 0: + # BAD: Should raise a KeyError or IndexError instead. + raise ValueError("Invalid index") + return self.lookup(idx) + diff --git a/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod3.py b/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod3.py new file mode 100644 index 000000000000..84ce9d18d275 --- /dev/null +++ b/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod3.py @@ -0,0 +1,4 @@ +class D: + def __hash__(self): + # BAD: Use `__hash__ = None` instead. + raise NotImplementedError(f"{self.__type__} is unhashable.") \ No newline at end of file From 3525e83ad2d8609491931c9e46ae9431dc6981aa Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Fri, 25 Jul 2025 09:52:54 +0100 Subject: [PATCH 06/11] Add changenote + some doc updates --- python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp | 6 +++--- python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql | 2 +- .../2025-07-25-unexpected-raise-special-method.md | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 python/ql/src/change-notes/2025-07-25-unexpected-raise-special-method.md diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp index a0c3463b9d17..d6ce2167b8c4 100644 --- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp +++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp @@ -20,18 +20,18 @@ Therefore, if a method is unable to perform the expected operation then its resp

      -
    • Attribute access, a.b (__getattr__): Raise AttributeError
    • +
    • Attribute access, a.b (__getattr__): Raise AttributeError.
    • Arithmetic operations, a + b (__add__): Do not raise an exception, return NotImplemented instead.
    • Indexing, a[b] (__getitem__): Raise KeyError or IndexError.
    • Hashing, hash(a) (__hash__): Should not raise an exception. Use __hash__ = None to indicate that an object is unhashable rather than raising an exception.
    • Equality methods, a == b (__eq__): Never raise an exception, always return True or False.
    • Ordering comparison methods, a < b (__lt__): Raise a TypeError if the objects cannot be ordered.
    • -
    • Most others: Ideally, do not implement the method at all, otherwise raise TypeError to indicate that the operation is unsupported.
    • +
    • Most others: If the operation is never supported, the method often does not need to be implemented at all; otherwise a TypeError should be raised.
    -

    If the method is intended to be abstract, then declare it so using the @abstractmethod decorator. +

    If the method is intended to be abstract, and thus always raise an exception, then declare it so using the @abstractmethod decorator. Otherwise, either remove the method or ensure that the method raises an exception of the correct type.

    diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql index 12107821aa66..3232ef51a2d3 100644 --- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql +++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql @@ -94,7 +94,7 @@ predicate preferredRaise(string name, string execName, string message) { } predicate execIsOfType(Expr exec, string execName) { - // Might make sense to have execName be an IPA type here. Or part of a more general API modelling builtin/stdlib subclass relations. + // Might make sense to have execName be an IPA type here. Or part of a more general API modeling builtin/stdlib subclass relations. exists(string subclass | execName = "TypeError" and subclass = "TypeError" diff --git a/python/ql/src/change-notes/2025-07-25-unexpected-raise-special-method.md b/python/ql/src/change-notes/2025-07-25-unexpected-raise-special-method.md new file mode 100644 index 000000000000..4b79dbc3b81e --- /dev/null +++ b/python/ql/src/change-notes/2025-07-25-unexpected-raise-special-method.md @@ -0,0 +1,5 @@ +--- +category: minorAnalysis +--- +* The `py/unexpected-raise-in-special-method` query has been modernized. It produces additional results in cases where the exception is +only raised conditionally. Its precision has been changed from `very-high` to `high`. \ No newline at end of file From 8bdf6801b3b92b40135dcfbab56a75c6f75b0ad8 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Fri, 25 Jul 2025 10:05:09 +0100 Subject: [PATCH 07/11] Add qldoc --- .../IncorrectRaiseInSpecialMethod.ql | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql index 3232ef51a2d3..fbb02822bf7a 100644 --- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql +++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql @@ -15,14 +15,17 @@ import python import semmle.python.ApiGraphs import semmle.python.dataflow.new.internal.DataFlowDispatch +/** Holds if `name` is the name of a special method for attribute access such as `a.b`, that should raise an `AttributeError`. */ private predicate attributeMethod(string name) { name = ["__getattribute__", "__getattr__", "__delattr__"] // __setattr__ excluded as it makes sense to raise different kinds of errors based on the `value` parameter } +/** Holds if `name` is the name of a special method for indexing operations such as `a[b]`, that should raise a `LookupError`. */ private predicate indexingMethod(string name) { name = ["__getitem__", "__delitem__"] // __setitem__ excluded as it makes sense to raise different kinds of errors based on the `value` parameter } +/** Holds if `name` is the name of a special method for arithmetic operations. */ private predicate arithmeticMethod(string name) { name = [ @@ -35,6 +38,7 @@ private predicate arithmeticMethod(string name) { ] } +/** Holds if `name is the name of a special method for ordering operations such as `a < b`. */ private predicate orderingMethod(string name) { name = [ @@ -45,6 +49,7 @@ private predicate orderingMethod(string name) { ] } +/** Holds if `name` is the name of a special method for casting an object to a numeric type, such as `int(x)` */ private predicate castMethod(string name) { name = [ @@ -53,9 +58,10 @@ private predicate castMethod(string name) { "__index__", "__trunc__", "__complex__" - ] + ] // __bool__ excluded as it makes sense to allow it to always raise } +/** Holds if we allow a special method named `name` to raise `exec` as an exception. */ predicate correctRaise(string name, Expr exec) { execIsOfType(exec, "TypeError") and ( @@ -71,6 +77,7 @@ predicate correctRaise(string name, Expr exec) { ) } +/** Holds if it is preferred for `name` to raise exceptions of type `execName`. `message` is the alert message. */ predicate preferredRaise(string name, string execName, string message) { attributeMethod(name) and execName = "AttributeError" and @@ -93,6 +100,7 @@ predicate preferredRaise(string name, string execName, string message) { message = "should raise a TypeError instead." } +/** Holds if `exec` is an exception object of the type named `execName`. */ predicate execIsOfType(Expr exec, string execName) { // Might make sense to have execName be an IPA type here. Or part of a more general API modeling builtin/stdlib subclass relations. exists(string subclass | @@ -114,6 +122,10 @@ predicate execIsOfType(Expr exec, string execName) { ) } +/** + * Holds if `meth` need not be implemented if it always raises. `message` is the alert message, and `allowNotImplemented` is true + * if we still allow the method to always raise `NotImplementedError`. + */ predicate noNeedToAlwaysRaise(Function meth, string message, boolean allowNotImplemented) { meth.getName() = "__hash__" and message = "use __hash__ = None instead." and @@ -130,14 +142,17 @@ predicate noNeedToAlwaysRaise(Function meth, string message, boolean allowNotImp ) } +/** Holds if `func` has a decorator likely marking it as an abstract method. */ predicate isAbstract(Function func) { func.getADecorator().(Name).getId().matches("%abstract%") } +/** Holds if `f` always raises the exception `exec`. */ predicate alwaysRaises(Function f, Expr exec) { directlyRaises(f, exec) and strictcount(Expr e | directlyRaises(f, e)) = 1 and not exists(f.getANormalExit()) } +/** Holds if `f` directly raises `expr` using a `raise` statement. */ predicate directlyRaises(Function f, Expr exec) { exists(Raise r | r.getScope() = f and @@ -146,10 +161,12 @@ predicate directlyRaises(Function f, Expr exec) { ) } +/** Holds if `exec` is a `NotImplementedError`. */ predicate isNotImplementedError(Expr exec) { exec = API::builtin("NotImplementedError").getACall().asExpr() } +/** Gets the name of the builtin exception type `exec` constructs, if it can be determined. */ string getExecName(Expr exec) { result = exec.(Call).getFunc().(Name).getId() } from Function f, Expr exec, string message From 9af2ab83dc66df7fd48501bf3f6ed75c2b6bba35 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Fri, 25 Jul 2025 10:22:51 +0100 Subject: [PATCH 08/11] Cleanups --- .../src/Functions/IncorrectRaiseInSpecialMethod.ql | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql index fbb02822bf7a..07c6fb1c5d37 100644 --- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql +++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql @@ -30,11 +30,11 @@ private predicate arithmeticMethod(string name) { name = [ "__add__", "__sub__", "__or__", "__xor__", "__rshift__", "__pow__", "__mul__", "__neg__", - "__radd__", "__rsub__", "__rdiv__", "__rfloordiv__", "__div__", "__rdiv__", "__rlshift__", - "__rand__", "__ror__", "__rxor__", "__rrshift__", "__rpow__", "__rmul__", "__truediv__", - "__rtruediv__", "__pos__", "__iadd__", "__isub__", "__idiv__", "__ifloordiv__", "__idiv__", - "__ilshift__", "__iand__", "__ior__", "__ixor__", "__irshift__", "__abs__", "__ipow__", - "__imul__", "__itruediv__", "__floordiv__", "__div__", "__divmod__", "__lshift__", "__and__" + "__radd__", "__rsub__", "__rdiv__", "__rfloordiv__", "__rlshift__", "__rand__", "__ror__", + "__rxor__", "__rrshift__", "__rpow__", "__rmul__", "__truediv__", "__rtruediv__", "__pos__", + "__iadd__", "__isub__", "__idiv__", "__ifloordiv__", "__idiv__", "__ilshift__", "__iand__", + "__ior__", "__ixor__", "__irshift__", "__abs__", "__ipow__", "__imul__", "__itruediv__", + "__floordiv__", "__div__", "__divmod__", "__lshift__", "__and__" ] } @@ -152,7 +152,7 @@ predicate alwaysRaises(Function f, Expr exec) { not exists(f.getANormalExit()) } -/** Holds if `f` directly raises `expr` using a `raise` statement. */ +/** Holds if `f` directly raises `exec` using a `raise` statement. */ predicate directlyRaises(Function f, Expr exec) { exists(Raise r | r.getScope() = f and From d7b855c4e379fef782a45f79e07ecc3305a6cc54 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Fri, 25 Jul 2025 10:24:58 +0100 Subject: [PATCH 09/11] qhelp fix --- python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp index d6ce2167b8c4..42d7d421b0a6 100644 --- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp +++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.qhelp @@ -49,7 +49,7 @@ In the following example, the __getitem__ method of C

    -In the following example, the class __hash__ method of D raises TypeError. +In the following example, the class __hash__ method of D raises NotImplementedError. This causes D to be incorrectly identified as hashable by isinstance(obj, collections.abc.Hashable); so the correct way to make a class unhashable is to set __hash__ = None.

    From 958fddb638b4ae13f2682b1fe984fc5af67e3138 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Fri, 25 Jul 2025 10:57:19 +0100 Subject: [PATCH 10/11] cleanup order and remove duplicates for arithmetic methods --- .../src/Functions/IncorrectRaiseInSpecialMethod.ql | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql index 07c6fb1c5d37..3cd7e0fe9871 100644 --- a/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql +++ b/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql @@ -29,12 +29,13 @@ private predicate indexingMethod(string name) { private predicate arithmeticMethod(string name) { name = [ - "__add__", "__sub__", "__or__", "__xor__", "__rshift__", "__pow__", "__mul__", "__neg__", - "__radd__", "__rsub__", "__rdiv__", "__rfloordiv__", "__rlshift__", "__rand__", "__ror__", - "__rxor__", "__rrshift__", "__rpow__", "__rmul__", "__truediv__", "__rtruediv__", "__pos__", - "__iadd__", "__isub__", "__idiv__", "__ifloordiv__", "__idiv__", "__ilshift__", "__iand__", - "__ior__", "__ixor__", "__irshift__", "__abs__", "__ipow__", "__imul__", "__itruediv__", - "__floordiv__", "__div__", "__divmod__", "__lshift__", "__and__" + "__add__", "__sub__", "__and__", "__or__", "__xor__", "__lshift__", "__rshift__", "__pow__", + "__mul__", "__div__", "__divmod__", "__truediv__", "__floordiv__", "__matmul__", "__radd__", + "__rsub__", "__rand__", "__ror__", "__rxor__", "__rlshift__", "__rrshift__", "__rpow__", + "__rmul__", "__rdiv__", "__rdivmod__", "__rtruediv__", "__rfloordiv__", "__rmatmul__", + "__iadd__", "__isub__", "__iand__", "__ior__", "__ixor__", "__ilshift__", "__irshift__", + "__ipow__", "__imul__", "__idiv__", "__idivmod__", "__itruediv__", "__ifloordiv__", + "__imatmul__", "__pos__", "__neg__", "__abs__", "__invert__", ] } From c0da9c407e12535984b98370f84a4f176bb17b34 Mon Sep 17 00:00:00 2001 From: Joe Farebrother Date: Fri, 25 Jul 2025 13:15:46 +0100 Subject: [PATCH 11/11] Fix typo in test dir name + update examples --- .../src/Functions/examples/IncorrectRaiseInSpecialMethod.py | 4 ++-- .../src/Functions/examples/IncorrectRaiseInSpecialMethod3.py | 2 +- .../IncorrectRaiseInSpecialMethod.expected | 0 .../IncorrectRaiseInSpecialMethod.qlref | 0 .../test.py | 0 5 files changed, 3 insertions(+), 3 deletions(-) rename python/ql/test/query-tests/Functions/{IncorrectRaiseInSpcialMethod => IncorrectRaiseInSpecialMethod}/IncorrectRaiseInSpecialMethod.expected (100%) rename python/ql/test/query-tests/Functions/{IncorrectRaiseInSpcialMethod => IncorrectRaiseInSpecialMethod}/IncorrectRaiseInSpecialMethod.qlref (100%) rename python/ql/test/query-tests/Functions/{IncorrectRaiseInSpcialMethod => IncorrectRaiseInSpecialMethod}/test.py (100%) diff --git a/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod.py b/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod.py index 77c623bef794..d565a86cab27 100644 --- a/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod.py +++ b/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod.py @@ -5,7 +5,7 @@ def __init__(self, a): def __add__(self, other): # BAD: Should return NotImplemented instead of raising if not isinstance(other,A): - raise TypeError(f"Cannot add A to {other.__type__}") + raise TypeError(f"Cannot add A to {other.__class__}") return A(self.a + other.a) class B: @@ -13,7 +13,7 @@ def __init__(self, a): self.a = a def __add__(self, other): - # GOOD: Returning NotImplemented allows for other classes to support adding do B. + # GOOD: Returning NotImplemented allows for the operation to fallback to other implementations to allow other classes to support adding to B. if not isinstance(other,B): return NotImplemented return B(self.a + other.a) diff --git a/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod3.py b/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod3.py index 84ce9d18d275..33541adc7e64 100644 --- a/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod3.py +++ b/python/ql/src/Functions/examples/IncorrectRaiseInSpecialMethod3.py @@ -1,4 +1,4 @@ class D: def __hash__(self): # BAD: Use `__hash__ = None` instead. - raise NotImplementedError(f"{self.__type__} is unhashable.") \ No newline at end of file + raise NotImplementedError(f"{self.__class__} is unhashable.") \ No newline at end of file diff --git a/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.expected b/python/ql/test/query-tests/Functions/IncorrectRaiseInSpecialMethod/IncorrectRaiseInSpecialMethod.expected similarity index 100% rename from python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.expected rename to python/ql/test/query-tests/Functions/IncorrectRaiseInSpecialMethod/IncorrectRaiseInSpecialMethod.expected diff --git a/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.qlref b/python/ql/test/query-tests/Functions/IncorrectRaiseInSpecialMethod/IncorrectRaiseInSpecialMethod.qlref similarity index 100% rename from python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/IncorrectRaiseInSpecialMethod.qlref rename to python/ql/test/query-tests/Functions/IncorrectRaiseInSpecialMethod/IncorrectRaiseInSpecialMethod.qlref diff --git a/python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/test.py b/python/ql/test/query-tests/Functions/IncorrectRaiseInSpecialMethod/test.py similarity index 100% rename from python/ql/test/query-tests/Functions/IncorrectRaiseInSpcialMethod/test.py rename to python/ql/test/query-tests/Functions/IncorrectRaiseInSpecialMethod/test.py