mirror of
https://github.com/github/codeql.git
synced 2026-02-15 14:33:40 +01:00
195 lines
7.2 KiB
Plaintext
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)
|