-
Notifications
You must be signed in to change notification settings - Fork 2k
Expand file tree
/
Copy pathSignatureOverriddenMethod.ql
More file actions
201 lines (185 loc) · 6.55 KB
/
SignatureOverriddenMethod.ql
File metadata and controls
201 lines (185 loc) · 6.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
/**
* @name Signature mismatch in overriding method
* @description Overriding a method without ensuring that both methods accept the same
* number and type of parameters has the potential to cause an error when there is a mismatch.
* @kind problem
* @problem.severity warning
* @tags quality
* reliability
* correctness
* @sub-severity high
* @precision very-high
* @id py/inheritance/signature-mismatch
*/
import python
import semmle.python.dataflow.new.DataFlow
import semmle.python.dataflow.new.internal.DataFlowDispatch
import codeql.util.Option
predicate overrides(Function base, Function sub) {
base.getName() = sub.getName() and
base.getScope() = getADirectSuperclass+(sub.getScope())
}
bindingset[num, str]
string plural(int num, string str) {
num = 1 and result = "1 " + str
or
num != 1 and result = num.toString() + " " + str + "s"
}
/** Holds if no way to call `base` would be valid for `sub`. The `msg` applies to the `sub method. */
predicate strongSignatureMismatch(Function base, Function sub, string msg) {
overrides(base, sub) and
(
sub.getMinPositionalArguments() > base.getMaxPositionalArguments() and
msg =
"requires " +
plural(sub.getMinPositionalArguments() - base.getMaxPositionalArguments(),
"more positional argument") + " than overridden $@ allows."
or
sub.getMaxPositionalArguments() < base.getMinPositionalArguments() and
msg =
"requires " +
plural(base.getMinPositionalArguments() - sub.getMaxPositionalArguments(),
"fewer positional argument") + " than overridden $@ allows."
)
}
/** Holds if there may be some ways to call `base` that would not be valid for `sub`. The `msg` applies to the `sub` method. */
predicate weakSignatureMismatch(Function base, Function sub, string msg) {
overrides(base, sub) and
(
sub.getMinPositionalArguments() > base.getMinPositionalArguments() and
msg =
"requires " +
plural(sub.getMinPositionalArguments() - base.getMinPositionalArguments(),
"more positional argument") + " than some possible calls to overridden $@."
or
sub.getMaxPositionalArguments() < base.getMaxPositionalArguments() and
msg =
"requires " +
plural(base.getMaxPositionalArguments() - sub.getMaxPositionalArguments(),
"fewer positional argument") + " than some possible calls to overridden $@."
or
sub.getMinPositionalArguments() <= base.getMinPositionalArguments() and
sub.getMaxPositionalArguments() >= base.getMaxPositionalArguments() and
exists(string arg |
// TODO: positional-only args not considered
// e.g. `def foo(x, y, /, z):` has x,y as positional only args, should not be considered as possible kw args
arg = base.getAnArg().getName() and
not arg = sub.getAnArg().getName() and
not exists(sub.getKwarg()) and
msg = "does not accept keyword argument `" + arg + "`, which overridden $@ does."
)
or
exists(base.getKwarg()) and
not exists(sub.getKwarg()) and
msg = "does not accept arbitrary keyword arguments, which overridden $@ does."
)
}
predicate ignore(Function f) {
isClassmethod(f)
or
exists(Function g |
g.getScope() = f.getScope() and
g.getName() = f.getName() and
g != f
)
}
Function resolveCall(Call call) {
exists(DataFlowCall dfc | call = dfc.getNode().(CallNode).getNode() |
result = viableCallable(dfc).(DataFlowFunction).getScope()
)
}
predicate callViableForEither(Function base, Function sub, Call call) {
overrides(base, sub) and
base = resolveCall(call) and
sub = resolveCall(call)
}
predicate matchingStatic(Function base, Function sub) {
overrides(base, sub) and
(
isStaticmethod(base) and
isStaticmethod(sub)
or
not isStaticmethod(base) and
not isStaticmethod(sub)
)
}
int extraSelfArg(Function func) { if isStaticmethod(func) then result = 0 else result = 1 }
predicate callMatchesSignature(Function func, Call call) {
(
call.getPositionalArgumentCount() + extraSelfArg(func) >= func.getMinPositionalArguments()
or
exists(call.getStarArg())
or
exists(call.getKwargs())
) and
call.getPositionalArgumentCount() + extraSelfArg(func) <= func.getMaxPositionalArguments() and
(
exists(func.getKwarg())
or
forall(string name | name = call.getANamedArgumentName() | exists(func.getArgByName(name)))
)
}
Call getASignatureMismatchWitness(Function base, Function sub) {
callViableForEither(base, sub, result) and
callMatchesSignature(base, result) and
not callMatchesSignature(sub, result)
}
Call chooseASignatureMismatchWitnessInFile(Function base, Function sub, File file) {
result =
min(Call c |
c = getASignatureMismatchWitness(base, sub) and
c.getLocation().getFile() = file
|
c order by c.getLocation().getStartLine(), c.getLocation().getStartColumn()
)
}
Call chooseASignatureMismatchWitness(Function base, Function sub) {
exists(getASignatureMismatchWitness(base, sub)) and
(
result = chooseASignatureMismatchWitnessInFile(base, sub, base.getLocation().getFile())
or
not exists(Call c |
c = getASignatureMismatchWitness(base, sub) and
c.getLocation().getFile() = base.getLocation().getFile()
) and
result = chooseASignatureMismatchWitnessInFile(base, sub, base.getLocation().getFile())
or
not exists(Call c |
c = getASignatureMismatchWitness(base, sub) and
c.getLocation().getFile() = [base, sub].getLocation().getFile()
) and
result =
min(Call c |
c = getASignatureMismatchWitness(base, sub)
|
c
order by
c.getLocation().getFile().getAbsolutePath(), c.getLocation().getStartLine(),
c.getLocation().getStartColumn()
)
)
}
module CallOption = LocOption2<Location, Call>;
from Function base, Function sub, string msg, string extraMsg, CallOption::Option call
where
not sub.isSpecialMethod() and
sub.getName() != "__init__" and
not ignore(sub) and
not ignore(base) and
matchingStatic(base, sub) and
(
call.asSome() = chooseASignatureMismatchWitness(base, sub) and
extraMsg =
" $@ correctly calls the base method, but does not match the signature of the overriding method." and
(
strongSignatureMismatch(base, sub, msg)
or
not strongSignatureMismatch(base, sub, _) and
weakSignatureMismatch(base, sub, msg)
)
or
not exists(getASignatureMismatchWitness(base, sub)) and
strongSignatureMismatch(base, sub, msg) and
extraMsg = ""
)
select sub, "This method " + msg + extraMsg, base, base.getQualifiedName(), call, "This call"