|
7 | 7 | * error-handling
|
8 | 8 | * @problem.severity recommendation
|
9 | 9 | * @sub-severity high
|
10 |
| - * @precision very-high |
| 10 | + * @precision high |
11 | 11 | * @id py/unexpected-raise-in-special-method
|
12 | 12 | */
|
13 | 13 |
|
14 | 14 | import python
|
| 15 | +import semmle.python.ApiGraphs |
| 16 | +import semmle.python.dataflow.new.internal.DataFlowDispatch |
15 | 17 |
|
16 |
| -private predicate attribute_method(string name) { |
17 |
| - name = "__getattribute__" or name = "__getattr__" or name = "__setattr__" |
| 18 | +/** Holds if `name` is the name of a special method for attribute access such as `a.b`, that should raise an `AttributeError`. */ |
| 19 | +private predicate attributeMethod(string name) { |
| 20 | + name = ["__getattribute__", "__getattr__", "__delattr__"] // __setattr__ excluded as it makes sense to raise different kinds of errors based on the `value` parameter |
18 | 21 | }
|
19 | 22 |
|
20 |
| -private predicate indexing_method(string name) { |
21 |
| - name = "__getitem__" or name = "__setitem__" or name = "__delitem__" |
| 23 | +/** Holds if `name` is the name of a special method for indexing operations such as `a[b]`, that should raise a `LookupError`. */ |
| 24 | +private predicate indexingMethod(string name) { |
| 25 | + name = ["__getitem__", "__delitem__"] // __setitem__ excluded as it makes sense to raise different kinds of errors based on the `value` parameter |
22 | 26 | }
|
23 | 27 |
|
24 |
| -private predicate arithmetic_method(string name) { |
25 |
| - name in [ |
26 |
| - "__add__", "__sub__", "__or__", "__xor__", "__rshift__", "__pow__", "__mul__", "__neg__", |
27 |
| - "__radd__", "__rsub__", "__rdiv__", "__rfloordiv__", "__div__", "__rdiv__", "__rlshift__", |
28 |
| - "__rand__", "__ror__", "__rxor__", "__rrshift__", "__rpow__", "__rmul__", "__truediv__", |
29 |
| - "__rtruediv__", "__pos__", "__iadd__", "__isub__", "__idiv__", "__ifloordiv__", "__idiv__", |
30 |
| - "__ilshift__", "__iand__", "__ior__", "__ixor__", "__irshift__", "__abs__", "__ipow__", |
31 |
| - "__imul__", "__itruediv__", "__floordiv__", "__div__", "__divmod__", "__lshift__", "__and__" |
| 28 | +/** Holds if `name` is the name of a special method for arithmetic operations. */ |
| 29 | +private predicate arithmeticMethod(string name) { |
| 30 | + name = |
| 31 | + [ |
| 32 | + "__add__", "__sub__", "__and__", "__or__", "__xor__", "__lshift__", "__rshift__", "__pow__", |
| 33 | + "__mul__", "__div__", "__divmod__", "__truediv__", "__floordiv__", "__matmul__", "__radd__", |
| 34 | + "__rsub__", "__rand__", "__ror__", "__rxor__", "__rlshift__", "__rrshift__", "__rpow__", |
| 35 | + "__rmul__", "__rdiv__", "__rdivmod__", "__rtruediv__", "__rfloordiv__", "__rmatmul__", |
| 36 | + "__iadd__", "__isub__", "__iand__", "__ior__", "__ixor__", "__ilshift__", "__irshift__", |
| 37 | + "__ipow__", "__imul__", "__idiv__", "__idivmod__", "__itruediv__", "__ifloordiv__", |
| 38 | + "__imatmul__", "__pos__", "__neg__", "__abs__", "__invert__", |
32 | 39 | ]
|
33 | 40 | }
|
34 | 41 |
|
35 |
| -private predicate ordering_method(string name) { |
36 |
| - name = "__lt__" |
37 |
| - or |
38 |
| - name = "__le__" |
39 |
| - or |
40 |
| - name = "__gt__" |
41 |
| - or |
42 |
| - name = "__ge__" |
43 |
| - or |
44 |
| - name = "__cmp__" and major_version() = 2 |
| 42 | +/** Holds if `name is the name of a special method for ordering operations such as `a < b`. */ |
| 43 | +private predicate orderingMethod(string name) { |
| 44 | + name = |
| 45 | + [ |
| 46 | + "__lt__", |
| 47 | + "__le__", |
| 48 | + "__gt__", |
| 49 | + "__ge__", |
| 50 | + ] |
45 | 51 | }
|
46 | 52 |
|
47 |
| -private predicate cast_method(string name) { |
48 |
| - name = "__nonzero__" and major_version() = 2 |
49 |
| - or |
50 |
| - name = "__int__" |
51 |
| - or |
52 |
| - name = "__float__" |
53 |
| - or |
54 |
| - name = "__long__" |
55 |
| - or |
56 |
| - name = "__trunc__" |
57 |
| - or |
58 |
| - name = "__complex__" |
| 53 | +/** Holds if `name` is the name of a special method for casting an object to a numeric type, such as `int(x)` */ |
| 54 | +private predicate castMethod(string name) { |
| 55 | + name = |
| 56 | + [ |
| 57 | + "__int__", |
| 58 | + "__float__", |
| 59 | + "__index__", |
| 60 | + "__trunc__", |
| 61 | + "__complex__" |
| 62 | + ] // __bool__ excluded as it makes sense to allow it to always raise |
59 | 63 | }
|
60 | 64 |
|
61 |
| -predicate correct_raise(string name, ClassObject ex) { |
62 |
| - ex.getAnImproperSuperType() = theTypeErrorType() and |
| 65 | +/** Holds if we allow a special method named `name` to raise `exec` as an exception. */ |
| 66 | +predicate correctRaise(string name, Expr exec) { |
| 67 | + execIsOfType(exec, "TypeError") and |
63 | 68 | (
|
64 |
| - name = "__copy__" or |
65 |
| - name = "__deepcopy__" or |
66 |
| - name = "__call__" or |
67 |
| - indexing_method(name) or |
68 |
| - attribute_method(name) |
| 69 | + indexingMethod(name) or |
| 70 | + attributeMethod(name) or |
| 71 | + // Allow add methods to raise a TypeError, as they can be used for sequence concatenation as well as arithmetic |
| 72 | + name = ["__add__", "__iadd__", "__radd__"] |
69 | 73 | )
|
70 | 74 | or
|
71 |
| - preferred_raise(name, ex) |
72 |
| - or |
73 |
| - preferred_raise(name, ex.getASuperType()) |
| 75 | + exists(string execName | |
| 76 | + preferredRaise(name, execName, _) and |
| 77 | + execIsOfType(exec, execName) |
| 78 | + ) |
74 | 79 | }
|
75 | 80 |
|
76 |
| -predicate preferred_raise(string name, ClassObject ex) { |
77 |
| - attribute_method(name) and ex = theAttributeErrorType() |
78 |
| - or |
79 |
| - indexing_method(name) and ex = Object::builtin("LookupError") |
80 |
| - or |
81 |
| - ordering_method(name) and ex = theTypeErrorType() |
82 |
| - or |
83 |
| - arithmetic_method(name) and ex = Object::builtin("ArithmeticError") |
84 |
| - or |
85 |
| - name = "__bool__" and ex = theTypeErrorType() |
| 81 | +/** Holds if it is preferred for `name` to raise exceptions of type `execName`. `message` is the alert message. */ |
| 82 | +predicate preferredRaise(string name, string execName, string message) { |
| 83 | + attributeMethod(name) and |
| 84 | + execName = "AttributeError" and |
| 85 | + message = "should raise an AttributeError instead." |
| 86 | + or |
| 87 | + indexingMethod(name) and |
| 88 | + execName = "LookupError" and |
| 89 | + message = "should raise a LookupError (KeyError or IndexError) instead." |
| 90 | + or |
| 91 | + orderingMethod(name) and |
| 92 | + execName = "TypeError" and |
| 93 | + message = "should raise a TypeError or return NotImplemented instead." |
| 94 | + or |
| 95 | + arithmeticMethod(name) and |
| 96 | + execName = "ArithmeticError" and |
| 97 | + message = "should raise an ArithmeticError or return NotImplemented instead." |
| 98 | + or |
| 99 | + name = "__bool__" and |
| 100 | + execName = "TypeError" and |
| 101 | + message = "should raise a TypeError instead." |
86 | 102 | }
|
87 | 103 |
|
88 |
| -predicate no_need_to_raise(string name, string message) { |
89 |
| - name = "__hash__" and message = "use __hash__ = None instead" |
90 |
| - or |
91 |
| - cast_method(name) and message = "there is no need to implement the method at all." |
| 104 | +/** Holds if `exec` is an exception object of the type named `execName`. */ |
| 105 | +predicate execIsOfType(Expr exec, string execName) { |
| 106 | + // Might make sense to have execName be an IPA type here. Or part of a more general API modeling builtin/stdlib subclass relations. |
| 107 | + exists(string subclass | |
| 108 | + execName = "TypeError" and |
| 109 | + subclass = "TypeError" |
| 110 | + or |
| 111 | + execName = "LookupError" and |
| 112 | + subclass = ["LookupError", "KeyError", "IndexError"] |
| 113 | + or |
| 114 | + execName = "ArithmeticError" and |
| 115 | + subclass = ["ArithmeticError", "FloatingPointError", "OverflowError", "ZeroDivisionError"] |
| 116 | + or |
| 117 | + execName = "AttributeError" and |
| 118 | + subclass = "AttributeError" |
| 119 | + | |
| 120 | + exec = API::builtin(subclass).getACall().asExpr() |
| 121 | + or |
| 122 | + exec = API::builtin(subclass).getASubclass().getACall().asExpr() |
| 123 | + ) |
| 124 | +} |
| 125 | + |
| 126 | +/** |
| 127 | + * Holds if `meth` need not be implemented if it always raises. `message` is the alert message, and `allowNotImplemented` is true |
| 128 | + * if we still allow the method to always raise `NotImplementedError`. |
| 129 | + */ |
| 130 | +predicate noNeedToAlwaysRaise(Function meth, string message, boolean allowNotImplemented) { |
| 131 | + meth.getName() = "__hash__" and |
| 132 | + message = "use __hash__ = None instead." and |
| 133 | + allowNotImplemented = false |
| 134 | + or |
| 135 | + castMethod(meth.getName()) and |
| 136 | + message = "this method does not need to be implemented." and |
| 137 | + allowNotImplemented = true and |
| 138 | + // Allow an always raising cast method if it's overriding other behavior |
| 139 | + not exists(Function overridden | |
| 140 | + overridden.getName() = meth.getName() and |
| 141 | + overridden.getScope() = getADirectSuperclass+(meth.getScope()) and |
| 142 | + not alwaysRaises(overridden, _) |
| 143 | + ) |
| 144 | +} |
| 145 | + |
| 146 | +/** Holds if `func` has a decorator likely marking it as an abstract method. */ |
| 147 | +predicate isAbstract(Function func) { func.getADecorator().(Name).getId().matches("%abstract%") } |
| 148 | + |
| 149 | +/** Holds if `f` always raises the exception `exec`. */ |
| 150 | +predicate alwaysRaises(Function f, Expr exec) { |
| 151 | + directlyRaises(f, exec) and |
| 152 | + strictcount(Expr e | directlyRaises(f, e)) = 1 and |
| 153 | + not exists(f.getANormalExit()) |
92 | 154 | }
|
93 | 155 |
|
94 |
| -predicate is_abstract(FunctionObject func) { |
95 |
| - func.getFunction().getADecorator().(Name).getId().matches("%abstract%") |
| 156 | +/** Holds if `f` directly raises `exec` using a `raise` statement. */ |
| 157 | +predicate directlyRaises(Function f, Expr exec) { |
| 158 | + exists(Raise r | |
| 159 | + r.getScope() = f and |
| 160 | + exec = r.getException() and |
| 161 | + exec instanceof Call |
| 162 | + ) |
96 | 163 | }
|
97 | 164 |
|
98 |
| -predicate always_raises(FunctionObject f, ClassObject ex) { |
99 |
| - ex = f.getARaisedType() and |
100 |
| - strictcount(f.getARaisedType()) = 1 and |
101 |
| - not exists(f.getFunction().getANormalExit()) and |
102 |
| - /* raising StopIteration is equivalent to a return in a generator */ |
103 |
| - not ex = theStopIterationType() |
| 165 | +/** Holds if `exec` is a `NotImplementedError`. */ |
| 166 | +predicate isNotImplementedError(Expr exec) { |
| 167 | + exec = API::builtin("NotImplementedError").getACall().asExpr() |
104 | 168 | }
|
105 | 169 |
|
106 |
| -from FunctionObject f, ClassObject cls, string message |
| 170 | +/** Gets the name of the builtin exception type `exec` constructs, if it can be determined. */ |
| 171 | +string getExecName(Expr exec) { result = exec.(Call).getFunc().(Name).getId() } |
| 172 | + |
| 173 | +from Function f, Expr exec, string message |
107 | 174 | where
|
108 |
| - f.getFunction().isSpecialMethod() and |
109 |
| - not is_abstract(f) and |
110 |
| - always_raises(f, cls) and |
| 175 | + f.isSpecialMethod() and |
| 176 | + not isAbstract(f) and |
| 177 | + directlyRaises(f, exec) and |
111 | 178 | (
|
112 |
| - no_need_to_raise(f.getName(), message) and not cls.getName() = "NotImplementedError" |
| 179 | + exists(boolean allowNotImplemented, string subMessage | |
| 180 | + alwaysRaises(f, exec) and |
| 181 | + noNeedToAlwaysRaise(f, subMessage, allowNotImplemented) and |
| 182 | + (allowNotImplemented = true implies not isNotImplementedError(exec)) and // don't alert if it's a NotImplementedError and that's ok |
| 183 | + message = "This method always raises $@ - " + subMessage |
| 184 | + ) |
113 | 185 | or
|
114 |
| - not correct_raise(f.getName(), cls) and |
115 |
| - not cls.getName() = "NotImplementedError" and |
116 |
| - exists(ClassObject preferred | preferred_raise(f.getName(), preferred) | |
117 |
| - message = "raise " + preferred.getName() + " instead" |
| 186 | + not isNotImplementedError(exec) and |
| 187 | + not correctRaise(f.getName(), exec) and |
| 188 | + exists(string subMessage | preferredRaise(f.getName(), _, subMessage) | |
| 189 | + if alwaysRaises(f, exec) |
| 190 | + then message = "This method always raises $@ - " + subMessage |
| 191 | + else message = "This method raises $@ - " + subMessage |
118 | 192 | )
|
119 | 193 | )
|
120 |
| -select f, "Function always raises $@; " + message, cls, cls.toString() |
| 194 | +select f, message, exec, getExecName(exec) |
0 commit comments