Skip to content

Commit 7156dee

Browse files
author
Sauyon Lee
authored
Merge pull request github#6521 from sauyon/java/test-gen-improvements
Java: generate more realistic tests
2 parents c8a5397 + b38a23d commit 7156dee

File tree

4 files changed

+848
-488
lines changed

4 files changed

+848
-488
lines changed

java/ql/src/utils/FlowTestCase.qll

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

0 commit comments

Comments
 (0)