Skip to content

Commit 73d257e

Browse files
Port unexpected raise away from pointsto
1 parent f432cf9 commit 73d257e

File tree

1 file changed

+101
-51
lines changed

1 file changed

+101
-51
lines changed

python/ql/src/Functions/IncorrectRaiseInSpecialMethod.ql

Lines changed: 101 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,18 @@
1212
*/
1313

1414
import python
15+
import semmle.python.ApiGraphs
16+
import semmle.python.dataflow.new.internal.DataFlowDispatch
1517

16-
private predicate attribute_method(string name) {
18+
private predicate attributeMethod(string name) {
1719
name = "__getattribute__" or name = "__getattr__" or name = "__setattr__"
1820
}
1921

20-
private predicate indexing_method(string name) {
22+
private predicate indexingMethod(string name) {
2123
name = "__getitem__" or name = "__setitem__" or name = "__delitem__"
2224
}
2325

24-
private predicate arithmetic_method(string name) {
26+
private predicate arithmeticMethod(string name) {
2527
name in [
2628
"__add__", "__sub__", "__or__", "__xor__", "__rshift__", "__pow__", "__mul__", "__neg__",
2729
"__radd__", "__rsub__", "__rdiv__", "__rfloordiv__", "__div__", "__rdiv__", "__rlshift__",
@@ -32,21 +34,17 @@ private predicate arithmetic_method(string name) {
3234
]
3335
}
3436

35-
private predicate ordering_method(string name) {
37+
private predicate orderingMethod(string name) {
3638
name = "__lt__"
3739
or
3840
name = "__le__"
3941
or
4042
name = "__gt__"
4143
or
4244
name = "__ge__"
43-
or
44-
name = "__cmp__" and major_version() = 2
4545
}
4646

47-
private predicate cast_method(string name) {
48-
name = "__nonzero__" and major_version() = 2
49-
or
47+
private predicate castMethod(string name) {
5048
name = "__int__"
5149
or
5250
name = "__float__"
@@ -58,63 +56,115 @@ private predicate cast_method(string name) {
5856
name = "__complex__"
5957
}
6058

61-
predicate correct_raise(string name, ClassObject ex) {
62-
ex.getAnImproperSuperType() = theTypeErrorType() and
59+
predicate correctRaise(string name, Expr exec) {
60+
execIsOfType(exec, "TypeError") and
6361
(
64-
name = "__copy__" or
65-
name = "__deepcopy__" or
66-
name = "__call__" or
67-
indexing_method(name) or
68-
attribute_method(name)
62+
indexingMethod(name) or
63+
attributeMethod(name)
6964
)
7065
or
71-
preferred_raise(name, ex)
72-
or
73-
preferred_raise(name, ex.getASuperType())
66+
exists(string execName |
67+
preferredRaise(name, execName, _) and
68+
execIsOfType(exec, execName)
69+
)
7470
}
7571

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()
72+
predicate preferredRaise(string name, string execName, string message) {
73+
// TODO: execName should be an IPA type
74+
attributeMethod(name) and
75+
execName = "AttributeError" and
76+
message = "should raise an AttributeError instead."
77+
or
78+
indexingMethod(name) and
79+
execName = "LookupError" and
80+
message = "should raise a LookupError (KeyError or IndexError) instead."
81+
or
82+
orderingMethod(name) and
83+
execName = "TypeError" and
84+
message = "should raise a TypeError, or return NotImplemented instead."
85+
or
86+
arithmeticMethod(name) and
87+
execName = "ArithmeticError" and
88+
message = "should raise an ArithmeticError, or return NotImplemented instead."
89+
or
90+
name = "__bool__" and
91+
execName = "TypeError" and
92+
message = "should raise a TypeError instead."
8693
}
8794

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."
95+
predicate execIsOfType(Expr exec, string execName) {
96+
exists(string subclass |
97+
execName = "TypeError" and
98+
subclass = "TypeError"
99+
or
100+
execName = "LookupError" and
101+
subclass = ["LookupError", "KeyError", "IndexError"]
102+
or
103+
execName = "ArithmeticError" and
104+
subclass = ["ArithmeticError", "FloatingPointError", "OverflowError", "ZeroDivisionError"]
105+
or
106+
execName = "AttributeError" and
107+
subclass = "AttributeError"
108+
|
109+
exec = API::builtin(subclass).getACall().asExpr()
110+
or
111+
exec = API::builtin(subclass).getASubclass().getACall().asExpr()
112+
)
92113
}
93114

94-
predicate is_abstract(FunctionObject func) {
95-
func.getFunction().getADecorator().(Name).getId().matches("%abstract%")
115+
predicate noNeedToAlwaysRaise(Function meth, string message, boolean allowNotImplemented) {
116+
meth.getName() = "__hash__" and
117+
message = "use __hash__ = None instead." and
118+
allowNotImplemented = false
119+
or
120+
castMethod(meth.getName()) and
121+
message = "this method does not need to be implemented." and
122+
allowNotImplemented = true and
123+
not exists(Function overridden |
124+
overridden.getName() = meth.getName() and
125+
overridden.getScope() = getADirectSuperclass+(meth.getScope()) and
126+
alwaysRaises(overridden, _)
127+
)
96128
}
97129

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()
130+
predicate isAbstract(Function func) { func.getADecorator().(Name).getId().matches("%abstract%") }
131+
132+
predicate alwaysRaises(Function f, Expr exec) {
133+
directlyRaises(f, exec) and
134+
strictcount(Expr e | directlyRaises(f, e)) = 1 and
135+
not exists(f.getANormalExit())
104136
}
105137

106-
from FunctionObject f, ClassObject cls, string message
138+
predicate directlyRaises(Function f, Expr exec) {
139+
exists(Raise r |
140+
r.getScope() = f and
141+
exec = r.getException() and
142+
not exec = API::builtin("StopIteration").asSource().asExpr()
143+
)
144+
}
145+
146+
predicate isNotImplementedError(Expr exec) {
147+
exec = API::builtin("NotImplementedError").getACall().asExpr()
148+
}
149+
150+
from Function f, Expr exec, string message
107151
where
108-
f.getFunction().isSpecialMethod() and
109-
not is_abstract(f) and
110-
always_raises(f, cls) and
152+
f.isSpecialMethod() and
153+
not isAbstract(f) and
154+
directlyRaises(f, exec) and
111155
(
112-
no_need_to_raise(f.getName(), message) and not cls.getName() = "NotImplementedError"
156+
exists(boolean allowNotImplemented, string subMessage |
157+
alwaysRaises(f, exec) and
158+
noNeedToAlwaysRaise(f, subMessage, allowNotImplemented) and
159+
(allowNotImplemented = false or not isNotImplementedError(exec)) and
160+
message = "This method always raises $@ - " + subMessage
161+
)
113162
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"
163+
alwaysRaises(f, exec) and // for now consider only alwaysRaises cases as original query
164+
not isNotImplementedError(exec) and
165+
not correctRaise(f.getName(), exec) and
166+
exists(string subMessage | preferredRaise(f.getName(), _, subMessage) |
167+
message = "This method always raises $@ - " + subMessage
118168
)
119169
)
120-
select f, "Function always raises $@; " + message, cls, cls.toString()
170+
select f, message, exec, exec.toString() // TODO: remove tostring

0 commit comments

Comments
 (0)