Files
codeql/python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql
2025-07-25 10:57:19 +01:00

195 lines
7.2 KiB
Plaintext

/**
* @name Non-standard exception raised in special method
* @description Raising a non-standard exception in a special method alters the expected interface of that method.
* @kind problem
* @tags quality
* reliability
* error-handling
* @problem.severity recommendation
* @sub-severity high
* @precision high
* @id py/unexpected-raise-in-special-method
*/
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 =
[
"__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__",
]
}
/** Holds if `name is the name of a special method for ordering operations such as `a < b`. */
private predicate orderingMethod(string name) {
name =
[
"__lt__",
"__le__",
"__gt__",
"__ge__",
]
}
/** 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 =
[
"__int__",
"__float__",
"__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
(
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
exists(string execName |
preferredRaise(name, execName, _) and
execIsOfType(exec, execName)
)
}
/** 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
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."
}
/** 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 |
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()
)
}
/**
* 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
allowNotImplemented = false
or
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
not alwaysRaises(overridden, _)
)
}
/** 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 `exec` using a `raise` statement. */
predicate directlyRaises(Function f, Expr exec) {
exists(Raise r |
r.getScope() = f and
exec = r.getException() and
exec instanceof Call
)
}
/** 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
where
f.isSpecialMethod() and
not isAbstract(f) and
directlyRaises(f, exec) and
(
exists(boolean allowNotImplemented, string subMessage |
alwaysRaises(f, exec) and
noNeedToAlwaysRaise(f, subMessage, allowNotImplemented) 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
not isNotImplementedError(exec) and
not correctRaise(f.getName(), exec) and
exists(string subMessage | preferredRaise(f.getName(), _, subMessage) |
if alwaysRaises(f, exec)
then message = "This method always raises $@ - " + subMessage
else message = "This method raises $@ - " + subMessage
)
)
select f, message, exec, getExecName(exec)