Skip to content

Commit 33c727e

Browse files
smowtonSauyon Lee
authored andcommitted
Split up GenerateFlowTestCase.qll
This doesn't change any behaviour or alter any predicate bodies
1 parent 039b655 commit 33c727e

File tree

4 files changed

+504
-484
lines changed

4 files changed

+504
-484
lines changed

java/ql/src/utils/FlowTestCase.qll

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
import java
2+
private import semmle.code.java.dataflow.internal.DataFlowUtil
3+
private import semmle.code.java.dataflow.ExternalFlow
4+
private import semmle.code.java.dataflow.FlowSummary
5+
private import semmle.code.java.dataflow.internal.FlowSummaryImpl
6+
private import FlowTestCaseUtils
7+
private import FlowTestCaseSupportMethods
8+
9+
/**
10+
* A CSV row to generate tests for. Users should extend this to define which
11+
* tests to generate. Rows specified here should also satisfy `SummaryModelCsv.row`.
12+
*/
13+
class TargetSummaryModelCsv extends Unit {
14+
/**
15+
* Holds if a test should be generated for `row`.
16+
*/
17+
abstract predicate row(string r);
18+
}
19+
20+
/**
21+
* Gets a CSV row for which a test has been requested, but `SummaryModelCsv.row` does not hold of it.
22+
*/
23+
query string missingSummaryModelCsv() {
24+
any(TargetSummaryModelCsv target).row(result) and
25+
not any(SummaryModelCsv model).row(result)
26+
}
27+
28+
/**
29+
* Returns type of parameter `i` of `callable`, including the type of `this` for parameter -1.
30+
*/
31+
Type getParameterType(CallableToTest callable, int i) {
32+
if i = -1 then result = callable.getDeclaringType() else result = callable.getParameterType(i)
33+
}
34+
35+
private class CallableToTest extends Callable {
36+
CallableToTest() {
37+
exists(
38+
string namespace, string type, boolean subtypes, string name, string signature, string ext
39+
|
40+
summaryModel(namespace, type, subtypes, name, signature, ext, _, _, _) and
41+
this = interpretElement(namespace, type, subtypes, name, signature, ext) and
42+
this.isPublic() and
43+
getRootType(this.getDeclaringType()).isPublic()
44+
)
45+
}
46+
}
47+
48+
/**
49+
* A test snippet (a fragment of Java code that checks that `row` causes `callable` to propagate value/taint (according to `preservesValue`)
50+
* from `input` to `output`). Usually there is one of these per CSV row (`row`), but there may be more if `row` describes more than one
51+
* override or overload of a particular method, or if the input or output specifications cover more than one argument.
52+
*/
53+
private newtype TTestCase =
54+
MkTestCase(
55+
CallableToTest callable, SummaryComponentStack input, SummaryComponentStack output, string kind,
56+
string row
57+
) {
58+
exists(
59+
string namespace, string type, boolean subtypes, string name, string signature, string ext,
60+
string inputSpec, string outputSpec
61+
|
62+
any(TargetSummaryModelCsv tsmc).row(row) and
63+
summaryModel(namespace, type, subtypes, name, signature, ext, inputSpec, outputSpec, kind, row) and
64+
callable = interpretElement(namespace, type, subtypes, name, signature, ext) and
65+
Private::External::interpretSpec(inputSpec, input) and
66+
Private::External::interpretSpec(outputSpec, output)
67+
)
68+
}
69+
70+
/**
71+
* A test snippet (as `TTestCase`, except `baseInput` and `baseOutput` hold the bottom of the summary stacks
72+
* `input` and `output` respectively (hence, `baseInput` and `baseOutput` are parameters or return values).
73+
*/
74+
class TestCase extends TTestCase {
75+
CallableToTest callable;
76+
SummaryComponentStack input;
77+
SummaryComponentStack output;
78+
SummaryComponentStack baseInput;
79+
SummaryComponentStack baseOutput;
80+
string kind;
81+
string row;
82+
83+
TestCase() {
84+
this = MkTestCase(callable, input, output, kind, row) and
85+
baseInput = input.drop(input.length() - 1) and
86+
baseOutput = output.drop(output.length() - 1)
87+
}
88+
89+
/**
90+
* Returns a representation of this test case's parameters suitable for debugging.
91+
*/
92+
string toString() {
93+
result =
94+
row + " / " + callable + " / " + input + " / " + output + " / " + baseInput + " / " +
95+
baseOutput + " / " + kind
96+
}
97+
98+
/**
99+
* Returns a value to pass as `callable`'s `argIdx`th argument whose value is irrelevant to the test
100+
* being generated. This will be a zero or a null value, perhaps typecast if we need to disambiguate overloads.
101+
*/
102+
string getFiller(int argIdx) {
103+
exists(Type t | t = callable.getParameterType(argIdx) |
104+
t instanceof RefType and
105+
(
106+
if mayBeAmbiguous(callable)
107+
then result = "(" + getShortNameIfPossible(t) + ")null"
108+
else result = "null"
109+
)
110+
or
111+
result = getZero(t)
112+
)
113+
}
114+
115+
/**
116+
* Returns the value to pass for `callable`'s `i`th argument, which may be `in` if this is the input argument for
117+
* this test, `out` if it is the output, `instance` if this is an instance method and the instance is neither the
118+
* input nor the output, or a zero/null filler value otherwise.
119+
*/
120+
string getArgument(int i) {
121+
(i = -1 or exists(callable.getParameter(i))) and
122+
if baseInput = SummaryComponentStack::argument(i)
123+
then result = "in"
124+
else
125+
if baseOutput = SummaryComponentStack::argument(i)
126+
then result = "out"
127+
else
128+
if i = -1
129+
then result = "instance"
130+
else result = this.getFiller(i)
131+
}
132+
133+
/**
134+
* Returns a statement invoking `callable`, passing `input` and capturing `output` as needed.
135+
*/
136+
string makeCall() {
137+
// For example, one of:
138+
// out = in.method(filler);
139+
// or
140+
// out = filler.method(filler, in, filler);
141+
// or
142+
// out = Type.method(filler, in, filler);
143+
// or
144+
// filler.method(filler, in, out, filler);
145+
// or
146+
// Type.method(filler, in, out, filler);
147+
// or
148+
// out = new Type(filler, in, filler);
149+
// or
150+
// new Type(filler, in, out, filler);
151+
// or
152+
// in.method(filler, out, filler);
153+
// or
154+
// out.method(filler, in, filler);
155+
exists(string storePrefix, string invokePrefix, string args |
156+
(
157+
if
158+
baseOutput = SummaryComponentStack::return()
159+
or
160+
callable instanceof Constructor and baseOutput = SummaryComponentStack::argument(-1)
161+
then storePrefix = "out = "
162+
else storePrefix = ""
163+
) and
164+
(
165+
if callable instanceof Constructor
166+
then invokePrefix = "new "
167+
else
168+
if callable.(Method).isStatic()
169+
then invokePrefix = getShortNameIfPossible(callable.getDeclaringType()) + "."
170+
else invokePrefix = this.getArgument(-1) + "."
171+
) and
172+
args = concat(int i | i >= 0 | this.getArgument(i), ", " order by i) and
173+
result = storePrefix + invokePrefix + callable.getName() + "(" + args + ")"
174+
)
175+
}
176+
177+
/**
178+
* Returns an inline test expectation appropriate to this CSV row.
179+
*/
180+
string getExpectation() {
181+
kind = "value" and result = "// $ hasValueFlow"
182+
or
183+
kind = "taint" and result = "// $ hasTaintFlow"
184+
}
185+
186+
/**
187+
* Returns a declaration and initialisation of a variable named `instance` if required; otherwise returns an empty string.
188+
*/
189+
string getInstancePrefix() {
190+
if
191+
callable instanceof Method and
192+
not callable.(Method).isStatic() and
193+
baseOutput != SummaryComponentStack::argument(-1) and
194+
baseInput != SummaryComponentStack::argument(-1)
195+
then
196+
// In this case `out` is the instance.
197+
result = getShortNameIfPossible(callable.getDeclaringType()) + " instance = null;\n\t\t\t"
198+
else result = ""
199+
}
200+
201+
/**
202+
* Returns the type of the output for this test.
203+
*/
204+
Type getOutputType() {
205+
if baseOutput = SummaryComponentStack::return()
206+
then result = callable.getReturnType()
207+
else
208+
exists(int i |
209+
baseOutput = SummaryComponentStack::argument(i) and
210+
result = getParameterType(callable, i)
211+
)
212+
}
213+
214+
/**
215+
* Returns the type of the input for this test.
216+
*/
217+
Type getInputType() {
218+
exists(int i |
219+
baseInput = SummaryComponentStack::argument(i) and
220+
result = getParameterType(callable, i)
221+
)
222+
}
223+
224+
/**
225+
* Returns the Java name for the type of the input to this test.
226+
*/
227+
string getInputTypeString() { result = getShortNameIfPossible(this.getInputType()) }
228+
229+
/**
230+
* Returns a call to `source()` wrapped in `newWith` methods as needed according to `input`.
231+
* For example, if the input specification is `ArrayElement of MapValue of Argument[0]`, this
232+
* will return `newWithMapValue(newWithArrayElement(source()))`.
233+
*/
234+
string getInput(SummaryComponentStack stack) {
235+
stack = input and result = "source()"
236+
or
237+
exists(SummaryComponentStack s |
238+
result = "newWith" + contentToken(getContent(s.head())) + "(" + this.getInput(s) + ")" and
239+
stack = s.tail()
240+
)
241+
}
242+
243+
/**
244+
* Returns `out` wrapped in `get` methods as needed according to `output`.
245+
* For example, if the output specification is `ArrayElement of MapValue of Argument[0]`, this
246+
* will return `getArrayElement(getMapValue(out))`.
247+
*/
248+
string getOutput(SummaryComponentStack componentStack) {
249+
componentStack = output.drop(_) and
250+
(
251+
if componentStack = baseOutput
252+
then result = "out"
253+
else
254+
result =
255+
"get" + contentToken(getContent(componentStack.head())) + "(" +
256+
this.getOutput(componentStack.tail()) + ")"
257+
)
258+
}
259+
260+
/**
261+
* Returns the definition of a `newWith` method needed to set up the input or a `get` method needed to set up the output for this test.
262+
*/
263+
string getASupportMethod() {
264+
result =
265+
"Object newWith" + contentToken(getContent(input.drop(_).head())) +
266+
"(Object element) { return null; }" or
267+
result =
268+
"Object get" + contentToken(getContent(output.drop(_).head())) +
269+
"(Object container) { return null; }"
270+
}
271+
272+
/**
273+
* Returns a CSV row describing a support method (`newWith` or `get` method) needed to set up the output for this test.
274+
*
275+
* For example, `newWithMapValue` will propagate a value from `Argument[0]` to `MapValue of ReturnValue`, and `getMapValue`
276+
* will do the opposite.
277+
*/
278+
string getASupportMethodModel() {
279+
exists(SummaryComponent c, string contentCsvDescription |
280+
c = input.drop(_).head() and contentCsvDescription = getComponentSpec(c)
281+
|
282+
result =
283+
"generatedtest;Test;false;newWith" + contentToken(getContent(c)) + ";;;Argument[0];" +
284+
contentCsvDescription + " of ReturnValue;value"
285+
)
286+
or
287+
exists(SummaryComponent c, string contentCsvDescription |
288+
c = output.drop(_).head() and contentCsvDescription = getComponentSpec(c)
289+
|
290+
result =
291+
"generatedtest;Test;false;get" + contentToken(getContent(c)) + ";;;" + contentCsvDescription
292+
+ " of Argument[0];ReturnValue;value"
293+
)
294+
}
295+
296+
/**
297+
* Gets an outer class name that this test would ideally import (and will, unless it clashes with another
298+
* type of the same name).
299+
*/
300+
Type getADesiredImport() {
301+
result =
302+
getRootSourceDeclaration([
303+
this.getOutputType(), this.getInputType(), callable.getDeclaringType()
304+
])
305+
or
306+
// Will refer to parameter types in disambiguating casts, like `(String)null`
307+
mayBeAmbiguous(callable) and result = getRootSourceDeclaration(callable.getAParamType())
308+
}
309+
310+
/**
311+
* Gets a test snippet (test body fragment) testing this `callable` propagates value or taint from
312+
* `input` to `output`, as specified by `row_` (which necessarily equals `row`).
313+
*/
314+
string getATestSnippetForRow(string row_) {
315+
row_ = row and
316+
result =
317+
"\t\t{\n\t\t\t// \"" + row + "\"\n\t\t\t" + getShortNameIfPossible(this.getOutputType()) +
318+
" out = null;\n\t\t\t" + this.getInputTypeString() + " in = (" + this.getInputTypeString() +
319+
")" + this.getInput(baseInput) + ";\n\t\t\t" + this.getInstancePrefix() + this.makeCall() +
320+
";\n\t\t\t" + "sink(" + this.getOutput(output) + "); " + this.getExpectation() + "\n\t\t}\n"
321+
}
322+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import java
2+
private import semmle.code.java.dataflow.internal.DataFlowUtil
3+
private import semmle.code.java.dataflow.ExternalFlow
4+
private import semmle.code.java.dataflow.FlowSummary
5+
private import semmle.code.java.dataflow.internal.FlowSummaryImpl
6+
private import FlowTestCaseUtils
7+
8+
/**
9+
* Returns a valid Java token naming the field `fc`.
10+
*/
11+
private string getFieldToken(FieldContent fc) {
12+
result =
13+
fc.getField().getDeclaringType().getSourceDeclaration().getName() + "_" +
14+
fc.getField().getName()
15+
}
16+
17+
/**
18+
* Returns a valid Java token naming the synthetic field `fc`,
19+
* assuming that the name of that field consists only of characters valid in a Java identifier and `.`.
20+
*/
21+
private string getSyntheticFieldToken(SyntheticFieldContent fc) {
22+
exists(string name, int parts |
23+
name = fc.getField() and
24+
parts = count(name.splitAt("."))
25+
|
26+
if parts = 1
27+
then result = name
28+
else result = name.splitAt(".", parts - 2) + "_" + name.splitAt(".", parts - 1)
29+
)
30+
}
31+
32+
/**
33+
* Returns a token suitable for incorporation into a Java method name describing content `c`.
34+
*/
35+
string contentToken(Content c) {
36+
c instanceof ArrayContent and result = "ArrayElement"
37+
or
38+
c instanceof CollectionContent and result = "Element"
39+
or
40+
c instanceof MapKeyContent and result = "MapKey"
41+
or
42+
c instanceof MapValueContent and result = "MapValue"
43+
or
44+
result = getFieldToken(c)
45+
or
46+
result = getSyntheticFieldToken(c)
47+
}
48+
49+
/**
50+
* Returns the `content` wrapped by `component`, if any.
51+
*/
52+
Content getContent(SummaryComponent component) { component = SummaryComponent::content(result) }

0 commit comments

Comments
 (0)