Skip to content

Commit 77642f2

Browse files
Update alert messages and choose one witness
1 parent 3e61fb8 commit 77642f2

File tree

1 file changed

+120
-14
lines changed

1 file changed

+120
-14
lines changed

python/ql/src/Functions/SignatureOverriddenMethod.ql

Lines changed: 120 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,36 @@
1313
*/
1414

1515
import python
16+
import semmle.python.dataflow.new.DataFlow
1617
import semmle.python.dataflow.new.internal.DataFlowDispatch
1718

1819
predicate overrides(Function base, Function sub) {
1920
base.getName() = sub.getName() and
20-
base.getScope() = getADirectSuperclass*(sub.getScope())
21+
base.getScope() = getADirectSuperclass+(sub.getScope())
22+
}
23+
24+
bindingset[num, str]
25+
string plural(int num, string str) {
26+
num = 1 and result = "1 " + str
27+
or
28+
num != 1 and result = num.toString() + " " + str + "s"
2129
}
2230

2331
/** Holds if no way to call `base` would be valid for `sub`. The `msg` applies to the `sub method. */
2432
predicate strongSignatureMismatch(Function base, Function sub, string msg) {
2533
overrides(base, sub) and
2634
(
2735
sub.getMinPositionalArguments() > base.getMaxPositionalArguments() and
28-
msg = "requires more positional arguments than overridden $@ allows."
36+
msg =
37+
"requires " +
38+
plural(sub.getMinPositionalArguments() - base.getMaxPositionalArguments(),
39+
"more positional argument") + " than overridden $@ allows."
2940
or
3041
sub.getMaxPositionalArguments() < base.getMinPositionalArguments() and
31-
msg = "requires fewer positional arguments than overridden $@ allows."
42+
msg =
43+
"requires " +
44+
plural(base.getMinPositionalArguments() - sub.getMaxPositionalArguments(),
45+
"fewer positional argument") + " than overridden $@ allows."
3246
)
3347
}
3448

@@ -37,18 +51,26 @@ predicate weakSignatureMismatch(Function base, Function sub, string msg) {
3751
overrides(base, sub) and
3852
(
3953
sub.getMinPositionalArguments() > base.getMinPositionalArguments() and
40-
msg = "requires more positional arguments than overridden $@ may accept."
54+
msg =
55+
"requires " +
56+
plural(sub.getMinPositionalArguments() - base.getMinPositionalArguments(),
57+
"more positional argument") + "than some possible calls to overridden $@."
4158
or
4259
sub.getMaxPositionalArguments() < base.getMaxPositionalArguments() and
43-
msg = "requires fewer positional arguments than overridden $@ may accept."
60+
msg =
61+
"requires " +
62+
plural(base.getMaxPositionalArguments() - sub.getMaxPositionalArguments(),
63+
"fewer positional argument") + " than some possible calls to overridden $@."
4464
or
65+
sub.getMinPositionalArguments() <= base.getMinPositionalArguments() and
66+
sub.getMaxPositionalArguments() >= base.getMaxPositionalArguments() and
4567
exists(string arg |
4668
// TODO: positional-only args not considered
4769
// e.g. `def foo(x, y, /, z):` has x,y as positional only args, should not be considered as possible kw args
4870
arg = base.getAnArg().getName() and
4971
not arg = sub.getAnArg().getName() and
5072
not exists(sub.getKwarg()) and
51-
msg = "does not accept keyword argument " + arg + ", which overridden $@ does."
73+
msg = "does not accept keyword argument `" + arg + "`, which overridden $@ does."
5274
)
5375
or
5476
exists(base.getKwarg()) and
@@ -67,16 +89,100 @@ predicate ignore(Function f) {
6789
)
6890
}
6991

70-
from Function base, Function sub, string msg
92+
Function resolveCall(Call call) {
93+
exists(DataFlowCall dfc | call = dfc.getNode().(CallNode).getNode() |
94+
result = viableCallable(dfc).(DataFlowFunction).getScope()
95+
)
96+
}
97+
98+
predicate callViableForEither(Function base, Function sub, Call call) {
99+
overrides(base, sub) and
100+
base = resolveCall(call) and
101+
sub = resolveCall(call)
102+
}
103+
104+
predicate matchingStatic(Function base, Function sub) {
105+
overrides(base, sub) and
106+
(
107+
isStaticmethod(base) and
108+
isStaticmethod(sub)
109+
or
110+
not isStaticmethod(base) and
111+
not isStaticmethod(sub)
112+
)
113+
}
114+
115+
int extraSelfArg(Function func) { if isStaticmethod(func) then result = 0 else result = 1 }
116+
117+
predicate callMatchesSignature(Function func, Call call) {
118+
(
119+
call.getPositionalArgumentCount() + extraSelfArg(func) >= func.getMinPositionalArguments()
120+
or
121+
exists(call.getStarArg())
122+
or
123+
exists(call.getKwargs())
124+
) and
125+
call.getPositionalArgumentCount() + extraSelfArg(func) <= func.getMaxPositionalArguments() and
126+
(
127+
exists(func.getKwarg())
128+
or
129+
forall(string name | name = call.getANamedArgumentName() | exists(func.getArgByName(name)))
130+
)
131+
}
132+
133+
Call getASignatureMismatchWitness(Function base, Function sub) {
134+
callViableForEither(base, sub, result) and
135+
callMatchesSignature(base, result) and
136+
not callMatchesSignature(sub, result)
137+
}
138+
139+
Call chooseASignatureMismatchWitnessInFile(Function base, Function sub, File file) {
140+
result =
141+
min(Call c |
142+
c = getASignatureMismatchWitness(base, sub) and
143+
c.getLocation().getFile() = file
144+
|
145+
c order by c.getLocation().getStartLine(), c.getLocation().getStartColumn()
146+
)
147+
}
148+
149+
Call chooseASignatureMismatchWitness(Function base, Function sub) {
150+
exists(getASignatureMismatchWitness(base, sub)) and
151+
(
152+
result = chooseASignatureMismatchWitnessInFile(base, sub, base.getLocation().getFile())
153+
or
154+
not exists(Call c |
155+
c = getASignatureMismatchWitness(base, sub) and
156+
c.getLocation().getFile() = base.getLocation().getFile()
157+
) and
158+
result = chooseASignatureMismatchWitnessInFile(base, sub, base.getLocation().getFile())
159+
or
160+
not exists(Call c |
161+
c = getASignatureMismatchWitness(base, sub) and
162+
c.getLocation().getFile() = [base, sub].getLocation().getFile()
163+
) and
164+
result =
165+
min(Call c |
166+
c = getASignatureMismatchWitness(base, sub)
167+
|
168+
c
169+
order by
170+
c.getLocation().getFile().getAbsolutePath(), c.getLocation().getStartLine(),
171+
c.getLocation().getStartColumn()
172+
)
173+
)
174+
}
175+
176+
from Function base, Function sub, string msg, string extraMsg, Call call
71177
where
72-
// not exists(base.getACall()) and
73-
// not exists(FunctionValue a_derived |
74-
// a_derived.overrides(base) and
75-
// exists(a_derived.getACall())
76-
// ) and
77178
not sub.isSpecialMethod() and
78179
sub.getName() != "__init__" and
79180
not ignore(sub) and
80181
not ignore(base) and
81-
strongSignatureMismatch(base, sub, msg)
82-
select sub, "This method " + msg, base, base.getQualifiedName()
182+
matchingStatic(base, sub) and
183+
weakSignatureMismatch(base, sub, msg) and
184+
//msg = " has a different signature to $@." and
185+
call = chooseASignatureMismatchWitness(base, sub) and
186+
extraMsg =
187+
" $@ correctly calls the base method, but does not match the signature of the overriding method."
188+
select sub, "This method " + msg + extraMsg, base, base.getQualifiedName(), call, "This call"

0 commit comments

Comments
 (0)