Skip to content

Commit ee2d18a

Browse files
authored
Merge pull request github#4665 from yoff/python-dataflow-modernize-tests
Python: Add new-style tests
2 parents bc41c26 + e786be0 commit ee2d18a

26 files changed

+995
-712
lines changed

python/ql/src/semmle/python/dataflow/new/internal/DataFlowPrivate.qll

-11
Original file line numberDiff line numberDiff line change
@@ -703,17 +703,6 @@ class SpecialCall extends DataFlowCall, TSpecialCall {
703703
}
704704
}
705705

706-
/** A data flow node that represents a call argument. */
707-
class ArgumentNode extends Node {
708-
ArgumentNode() { this = any(DataFlowCall c).getArg(_) }
709-
710-
/** Holds if this argument occurs at the given position in the given call. */
711-
predicate argumentOf(DataFlowCall call, int pos) { this = call.getArg(pos) }
712-
713-
/** Gets the call in which this node is an argument. */
714-
final DataFlowCall getCall() { this.argumentOf(result, _) }
715-
}
716-
717706
/** Gets a viable run-time target for the call `call`. */
718707
DataFlowCallable viableCallable(DataFlowCall call) { result = call.getCallable() }
719708

python/ql/src/semmle/python/dataflow/new/internal/DataFlowPublic.qll

+11
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,17 @@ class ParameterNode extends CfgNode {
193193
Parameter getParameter() { result = def.getParameter() }
194194
}
195195

196+
/** A data flow node that represents a call argument. */
197+
class ArgumentNode extends Node {
198+
ArgumentNode() { this = any(DataFlowCall c).getArg(_) }
199+
200+
/** Holds if this argument occurs at the given position in the given call. */
201+
predicate argumentOf(DataFlowCall call, int pos) { this = call.getArg(pos) }
202+
203+
/** Gets the call in which this node is an argument. */
204+
final DataFlowCall getCall() { this.argumentOf(result, _) }
205+
}
206+
196207
/**
197208
* A node associated with an object after an operation that might have
198209
* changed its state.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import python
2+
import semmle.python.dataflow.new.DataFlow
3+
import TestUtilities.InlineExpectationsTest
4+
import experimental.dataflow.TestUtil.PrintNode
5+
6+
abstract class FlowTest extends InlineExpectationsTest {
7+
bindingset[this]
8+
FlowTest() { any() }
9+
10+
abstract string flowTag();
11+
12+
abstract predicate relevantFlow(DataFlow::Node fromNode, DataFlow::Node toNode);
13+
14+
override string getARelevantTag() { result = this.flowTag() }
15+
16+
override predicate hasActualResult(Location location, string element, string tag, string value) {
17+
exists(DataFlow::Node fromNode, DataFlow::Node toNode | this.relevantFlow(fromNode, toNode) |
18+
location = toNode.getLocation() and
19+
tag = this.flowTag() and
20+
value =
21+
"\"" + prettyNode(fromNode).replaceAll("\"", "'") + lineStr(fromNode, toNode) + " -> " +
22+
prettyNode(toNode).replaceAll("\"", "'") + "\"" and
23+
element = toNode.toString()
24+
)
25+
}
26+
27+
pragma[inline]
28+
private string lineStr(DataFlow::Node fromNode, DataFlow::Node toNode) {
29+
exists(int delta |
30+
delta = fromNode.getLocation().getStartLine() - toNode.getLocation().getStartLine()
31+
|
32+
if delta = 0
33+
then result = ""
34+
else
35+
if delta > 0
36+
then result = ", l:+" + delta.toString()
37+
else result = ", l:" + delta.toString()
38+
)
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import python
2+
import semmle.python.dataflow.new.DataFlow
3+
import FlowTest
4+
5+
class LocalFlowStepTest extends FlowTest {
6+
LocalFlowStepTest() { this = "LocalFlowStepTest" }
7+
8+
override string flowTag() { result = "step" }
9+
10+
override predicate relevantFlow(DataFlow::Node fromNode, DataFlow::Node toNode) {
11+
DataFlow::localFlowStep(fromNode, toNode)
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import python
2+
import semmle.python.dataflow.new.DataFlow
3+
private import semmle.python.dataflow.new.internal.DataFlowPrivate
4+
import FlowTest
5+
6+
class MaximalFlowTest extends FlowTest {
7+
MaximalFlowTest() { this = "MaximalFlowTest" }
8+
9+
override string flowTag() { result = "flow" }
10+
11+
override predicate relevantFlow(DataFlow::Node source, DataFlow::Node sink) {
12+
source != sink and
13+
exists(MaximalFlowsConfig cfg | cfg.hasFlow(source, sink))
14+
}
15+
}
16+
17+
/**
18+
* A configuration to find all "maximal" flows.
19+
* To be used on small programs.
20+
*/
21+
class MaximalFlowsConfig extends DataFlow::Configuration {
22+
MaximalFlowsConfig() { this = "MaximalFlowsConfig" }
23+
24+
override predicate isSource(DataFlow::Node node) {
25+
exists(node.getLocation().getFile().getRelativePath()) and
26+
not node.asCfgNode() instanceof CallNode and
27+
not node.asCfgNode().getNode() instanceof Return and
28+
not node instanceof DataFlow::ParameterNode and
29+
not node instanceof DataFlow::PostUpdateNode and
30+
// not node.asExpr() instanceof FunctionExpr and
31+
// not node.asExpr() instanceof ClassExpr and
32+
not exists(DataFlow::Node pred | DataFlow::localFlowStep(pred, node))
33+
}
34+
35+
override predicate isSink(DataFlow::Node node) {
36+
exists(node.getLocation().getFile().getRelativePath()) and
37+
not any(CallNode c).getArg(_) = node.asCfgNode() and
38+
not node instanceof DataFlow::ArgumentNode and
39+
not node.asCfgNode().(NameNode).getId().matches("SINK%") and
40+
not exists(DataFlow::Node succ | DataFlow::localFlowStep(node, succ))
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import python
2+
import semmle.python.dataflow.new.DataFlow
3+
4+
string prettyExp(Expr e) {
5+
not e instanceof Num and
6+
not e instanceof StrConst and
7+
not e instanceof Subscript and
8+
not e instanceof Call and
9+
not e instanceof Attribute and
10+
result = e.toString()
11+
or
12+
result = e.(Num).getN()
13+
or
14+
result =
15+
e.(StrConst).getPrefix() + e.(StrConst).getText() +
16+
e.(StrConst).getPrefix().regexpReplaceAll("[a-zA-Z]+", "")
17+
or
18+
result = prettyExp(e.(Subscript).getObject()) + "[" + prettyExp(e.(Subscript).getIndex()) + "]"
19+
or
20+
(
21+
if exists(e.(Call).getAnArg()) or exists(e.(Call).getANamedArg())
22+
then result = prettyExp(e.(Call).getFunc()) + "(..)"
23+
else result = prettyExp(e.(Call).getFunc()) + "()"
24+
)
25+
or
26+
result = prettyExp(e.(Attribute).getObject()) + "." + e.(Attribute).getName()
27+
}
28+
29+
string prettyNode(DataFlow::Node node) {
30+
if exists(node.asExpr()) then result = prettyExp(node.asExpr()) else result = node.toString()
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import python
2+
import semmle.python.dataflow.new.DataFlow
3+
import TestUtilities.InlineExpectationsTest
4+
import experimental.dataflow.TestUtil.PrintNode
5+
6+
/**
7+
* A routing test is designed to test that values are routed to the
8+
* correct arguments of the correct functions. It is assumed that
9+
* the functions tested sink their arguments sequentially, that is
10+
* `SINK1(arg1)`, etc.
11+
*/
12+
abstract class RoutingTest extends InlineExpectationsTest {
13+
bindingset[this]
14+
RoutingTest() { any() }
15+
16+
abstract string flowTag();
17+
18+
abstract predicate relevantFlow(DataFlow::Node fromNode, DataFlow::Node toNode);
19+
20+
override string getARelevantTag() { result in ["func", this.flowTag()] }
21+
22+
override predicate hasActualResult(Location location, string element, string tag, string value) {
23+
exists(DataFlow::Node fromNode, DataFlow::Node toNode | this.relevantFlow(fromNode, toNode) |
24+
location = fromNode.getLocation() and
25+
element = fromNode.toString() and
26+
(
27+
tag = this.flowTag() and
28+
if "\"" + tag + "\"" = fromValue(fromNode) then value = "" else value = fromValue(fromNode)
29+
or
30+
tag = "func" and
31+
value = toFunc(toNode) and
32+
not value = fromFunc(fromNode)
33+
)
34+
)
35+
}
36+
37+
pragma[inline]
38+
private string fromValue(DataFlow::Node fromNode) {
39+
result = "\"" + prettyNode(fromNode).replaceAll("\"", "'") + "\""
40+
}
41+
42+
pragma[inline]
43+
private string fromFunc(DataFlow::ArgumentNode fromNode) {
44+
result = fromNode.getCall().getNode().(CallNode).getFunction().getNode().(Name).getId()
45+
}
46+
47+
pragma[inline]
48+
private string toFunc(DataFlow::Node toNode) {
49+
result = toNode.getEnclosingCallable().getCallableValue().getScope().getQualifiedName() // TODO: More robust pretty printing?
50+
}
51+
}

python/ql/test/experimental/dataflow/basic/localFlowStepTest.expected

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import experimental.dataflow.TestUtil.LocalFlowStepTest

python/ql/test/experimental/dataflow/basic/maximalFlowTest.expected

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import experimental.dataflow.TestUtil.MaximalFlowTest
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
def obfuscated_id(x):
2-
y = x
3-
z = y
4-
return z
1+
def obfuscated_id(x): #$ step="FunctionExpr -> GSSA Variable obfuscated_id" step="x -> SSA variable x"
2+
y = x #$ step="x -> SSA variable y" step="SSA variable x, l:-1 -> x"
3+
z = y #$ step="y -> SSA variable z" step="SSA variable y, l:-1 -> y"
4+
return z #$ flow="42, l:+2 -> z" step="SSA variable z, l:-1 -> z"
55

6-
a = 42
7-
b = obfuscated_id(a)
6+
a = 42 #$ step="42 -> GSSA Variable a"
7+
b = obfuscated_id(a) #$ flow="42, l:-1 -> GSSA Variable b" flow="FunctionExpr, l:-6 -> obfuscated_id" step="obfuscated_id(..) -> GSSA Variable b" step="GSSA Variable obfuscated_id, l:-6 -> obfuscated_id" step="GSSA Variable a, l:-1 -> a"

python/ql/test/experimental/dataflow/callGraphConfig.qll

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class CallGraphConfig extends DataFlow::Configuration {
1111
override predicate isSource(DataFlow::Node node) {
1212
node instanceof DataFlowPrivate::ReturnNode
1313
or
14-
node instanceof DataFlowPrivate::ArgumentNode
14+
node instanceof DataFlow::ArgumentNode
1515
}
1616

1717
override predicate isSink(DataFlow::Node node) {

python/ql/test/experimental/dataflow/coverage/argumentPassing.py

+21-21
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,12 @@ def argument_passing(
8686

8787
@expects(7)
8888
def test_argument_passing1():
89-
argument_passing(arg1, *(arg2, arg3, arg4), e=arg5, **{"f": arg6, "g": arg7})
89+
argument_passing(arg1, *(arg2, arg3, arg4), e=arg5, **{"f": arg6, "g": arg7}) #$ arg1 arg7 func=argument_passing MISSING: arg2 arg3="arg3 arg4 arg5 arg6
9090

9191

9292
@expects(7)
9393
def test_argument_passing2():
94-
argument_passing(arg1, arg2, arg3, f=arg6)
94+
argument_passing(arg1, arg2, arg3, f=arg6) #$ arg1 arg2 arg3
9595

9696

9797
def with_pos_only(a, /, b):
@@ -101,9 +101,9 @@ def with_pos_only(a, /, b):
101101

102102
@expects(6)
103103
def test_pos_only():
104-
with_pos_only(arg1, arg2)
105-
with_pos_only(arg1, b=arg2)
106-
with_pos_only(arg1, *(arg2,))
104+
with_pos_only(arg1, arg2) #$ arg1 arg2
105+
with_pos_only(arg1, b=arg2) #$ arg1 arg2
106+
with_pos_only(arg1, *(arg2,)) #$ arg1 MISSING: arg2
107107

108108

109109
def with_multiple_kw_args(a, b, c):
@@ -114,13 +114,13 @@ def with_multiple_kw_args(a, b, c):
114114

115115
@expects(12)
116116
def test_multiple_kw_args():
117-
with_multiple_kw_args(b=arg2, c=arg3, a=arg1)
118-
with_multiple_kw_args(arg1, *(arg2,), arg3)
119-
with_multiple_kw_args(arg1, **{"c": arg3}, b=arg2)
120-
with_multiple_kw_args(**{"b": arg2}, **{"c": arg3}, **{"a": arg1})
117+
with_multiple_kw_args(b=arg2, c=arg3, a=arg1) #$ arg1 arg2 arg3
118+
with_multiple_kw_args(arg1, *(arg2,), arg3) #$ arg1 MISSING: arg2 arg3
119+
with_multiple_kw_args(arg1, **{"c": arg3}, b=arg2) #$ arg1 arg3 func=with_multiple_kw_args MISSING: arg2
120+
with_multiple_kw_args(**{"b": arg2}, **{"c": arg3}, **{"a": arg1}) #$ arg1 arg2 arg3 func=with_multiple_kw_args
121121

122122

123-
def with_default_arguments(a=arg1, b=arg2, c=arg3):
123+
def with_default_arguments(a=arg1, b=arg2, c=arg3): # Need a mechanism to test default arguments
124124
SINK1(a)
125125
SINK2(b)
126126
SINK3(c)
@@ -129,9 +129,9 @@ def with_default_arguments(a=arg1, b=arg2, c=arg3):
129129
@expects(12)
130130
def test_default_arguments():
131131
with_default_arguments()
132-
with_default_arguments(arg1)
133-
with_default_arguments(b=arg2)
134-
with_default_arguments(**{"c": arg3})
132+
with_default_arguments(arg1) #$ arg1
133+
with_default_arguments(b=arg2) #$ arg2
134+
with_default_arguments(**{"c": arg3}) #$ arg3 func=with_default_arguments
135135

136136

137137
# Nested constructor pattern
@@ -157,55 +157,55 @@ def grab_baz(baz):
157157

158158
@expects(4)
159159
def test_grab():
160-
grab_foo_bar_baz(baz=arg3, bar=arg2, foo=arg1)
160+
grab_foo_bar_baz(baz=arg3, bar=arg2, foo=arg1) #$ arg1 arg2 arg3 func=grab_bar_baz func=grab_baz
161161

162162

163163
# All combinations
164164
def test_pos_pos():
165165
def with_pos(a):
166166
SINK1(a)
167167

168-
with_pos(arg1)
168+
with_pos(arg1) #$ arg1 func=test_pos_pos.with_pos
169169

170170

171171
def test_pos_pos_only():
172172
def with_pos_only(a, /):
173173
SINK1(a)
174174

175-
with_pos_only(arg1)
175+
with_pos_only(arg1) #$ arg1 func=test_pos_pos_only.with_pos_only
176176

177177

178178
def test_pos_star():
179179
def with_star(*a):
180180
if len(a) > 0:
181181
SINK1(a[0])
182182

183-
with_star(arg1)
183+
with_star(arg1) #$ arg1 func=test_pos_star.with_star
184184

185185

186186
def test_pos_kw():
187187
def with_kw(a=""):
188188
SINK1(a)
189189

190-
with_kw(arg1)
190+
with_kw(arg1) #$ arg1 func=test_pos_kw.with_kw
191191

192192

193193
def test_kw_pos():
194194
def with_pos(a):
195195
SINK1(a)
196196

197-
with_pos(a=arg1)
197+
with_pos(a=arg1) #$ arg1 func=test_kw_pos.with_pos
198198

199199

200200
def test_kw_kw():
201201
def with_kw(a=""):
202202
SINK1(a)
203203

204-
with_kw(a=arg1)
204+
with_kw(a=arg1) #$ arg1 func=test_kw_kw.with_kw
205205

206206

207207
def test_kw_doublestar():
208208
def with_doublestar(**a):
209209
SINK1(a["a"])
210210

211-
with_doublestar(a=arg1)
211+
with_doublestar(a=arg1) #$ arg1 func=test_kw_doublestar.with_doublestar

python/ql/test/experimental/dataflow/coverage/argumentRoutingTest.expected

Whitespace-only changes.

0 commit comments

Comments
 (0)