Skip to content

Commit 0d8124b

Browse files
committed
Document test generator
1 parent 6172019 commit 0d8124b

File tree

1 file changed

+116
-4
lines changed

1 file changed

+116
-4
lines changed

java/ql/src/utils/GenerateFlowTestCase.qll

Lines changed: 116 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,22 @@ import semmle.code.java.dataflow.ExternalFlow
44
import semmle.code.java.dataflow.FlowSummary
55
import semmle.code.java.dataflow.internal.FlowSummaryImpl
66

7+
/**
8+
* A CSV row to generate tests for. Users should extend this to define their input rows.
9+
*/
710
bindingset[this]
811
abstract class CsvRow extends string { }
912

13+
/**
14+
* Returns type of parameter `i` of `callable`, including the type of `this` for parameter -1.
15+
*/
1016
Type getParameterType(Private::External::SummarizedCallableExternal callable, int i) {
1117
if i = -1 then result = callable.getDeclaringType() else result = callable.getParameterType(i)
1218
}
1319

20+
/**
21+
* Returns a zero value of primitive type `t`.
22+
*/
1423
string getZero(PrimitiveType t) {
1524
t.hasName("float") and result = "0.0f"
1625
or
@@ -29,6 +38,9 @@ string getZero(PrimitiveType t) {
2938
t.hasName("long") and result = "0L"
3039
}
3140

41+
/**
42+
* Holds if `c` may require disambiguation from an overload with the same argument count.
43+
*/
3244
predicate mayBeAmbiguous(Callable c) {
3345
exists(Callable other, string package, string type, string name |
3446
c.hasQualifiedName(package, type, name) and
@@ -38,14 +50,23 @@ predicate mayBeAmbiguous(Callable c) {
3850
)
3951
}
4052

53+
/**
54+
* Returns the `content` wrapped by `component`, if any.
55+
*/
4156
Content getContent(SummaryComponent component) { component = SummaryComponent::content(result) }
4257

58+
/**
59+
* Returns a valid Java token naming the field `fc`.
60+
*/
4361
string getFieldToken(FieldContent fc) {
4462
result =
4563
fc.getField().getDeclaringType().getSourceDeclaration().getName() + "_" +
4664
fc.getField().getName()
4765
}
4866

67+
/**
68+
* Returns a token suitable for incorporation into a Java method name describing content `c`.
69+
*/
4970
string contentToken(Content c) {
5071
c instanceof ArrayContent and result = "ArrayElement"
5172
or
@@ -58,24 +79,38 @@ string contentToken(Content c) {
5879
result = getFieldToken(c)
5980
}
6081

82+
/**
83+
* Returns the outermost type enclosing type `t` (which may be `t` itself).
84+
*/
6185
RefType getRootType(RefType t) {
6286
if t instanceof NestedType
6387
then result = getRootType(t.(NestedType).getEnclosingType())
6488
else result = t
6589
}
6690

91+
/**
92+
* Returns `t`'s first upper bound if `t` is a type variable; otherwise returns `t`.
93+
*/
6794
RefType replaceTypeVariable(RefType t) {
6895
if t instanceof TypeVariable
6996
then result = t.(TypeVariable).getFirstUpperBoundType()
7097
else result = t
7198
}
7299

100+
/**
101+
* Returns `t`'s outermost enclosing type, in raw form (i.e. generic types are given without generic parameters, and type variables are replaced by their bounds).
102+
*/
73103
Type getRootSourceDeclaration(Type t) {
74104
if t instanceof RefType
75105
then result = getRootType(replaceTypeVariable(t)).getSourceDeclaration()
76106
else result = t
77107
}
78108

109+
/**
110+
* A test snippet (a fragment of Java code that checks that `row` causes `callable` to propagate value/taint (according to `preservesValue`)
111+
* 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
112+
* override or overload of a particular method, or if the input or output specifications cover more than one argument.
113+
*/
79114
newtype TRowTestSnippet =
80115
MkSnippet(
81116
CsvRow row, Private::External::SummarizedCallableExternal callable, SummaryComponentStack input,
@@ -84,6 +119,10 @@ newtype TRowTestSnippet =
84119
callable.propagatesFlowForRow(input, output, preservesValue, row)
85120
}
86121

122+
/**
123+
* A test snippet (as `TRowTestSnippet`, except `baseInput` and `baseOutput` hold the bottom of the summary stacks
124+
* `input` and `output` respectively (hence, `baseInput` and `baseOutput` are parameters or return values).
125+
*/
87126
class RowTestSnippet extends TRowTestSnippet {
88127
string row;
89128
Private::External::SummarizedCallableExternal callable;
@@ -105,19 +144,28 @@ class RowTestSnippet extends TRowTestSnippet {
105144
baseOutput + " / " + preservesValue
106145
}
107146

147+
/**
148+
* Returns a value to pass as `callable`'s `argIdx`th argument whose value is irrelevant to the test
149+
* being generated. This will be a zero or a null value, perhaps typecast if we need to disambiguate overloads.
150+
*/
108151
string getFiller(int argIdx) {
109152
exists(Type t | t = callable.getParameterType(argIdx) |
110153
t instanceof RefType and
111154
(
112155
if mayBeAmbiguous(callable)
113-
then result = "(" + getShortNameIfPossible(t.(RefType).getSourceDeclaration()) + ")null"
156+
then result = "(" + getShortNameIfPossible(t) + ")null"
114157
else result = "null"
115158
)
116159
or
117160
result = getZero(t)
118161
)
119162
}
120163

164+
/**
165+
* Returns the value to pass for `callable`'s `i`th argument, which may be `in` if this is the input argument for
166+
* this test, `out` if it is the output, `instance` if this is an instance method and the instance is neither the
167+
* input nor the output, or a zero/null filler value otherwise.
168+
*/
121169
string getArgument(int i) {
122170
(i = -1 or exists(callable.getParameter(i))) and
123171
if baseInput = SummaryComponentStack::argument(i)
@@ -131,7 +179,11 @@ class RowTestSnippet extends TRowTestSnippet {
131179
)
132180
}
133181

182+
/**
183+
* Returns a statement invoking `callable`, passing `input` and capturing `output` as needed.
184+
*/
134185
string makeCall() {
186+
// For example, one of:
135187
// out = in.method(filler);
136188
// or
137189
// out = filler.method(filler, in, filler);
@@ -159,22 +211,26 @@ class RowTestSnippet extends TRowTestSnippet {
159211
then invokePrefix = "new "
160212
else
161213
if callable.(Method).isStatic()
162-
then
163-
invokePrefix =
164-
getShortNameIfPossible(callable.getDeclaringType().getSourceDeclaration()) + "."
214+
then invokePrefix = getShortNameIfPossible(callable.getDeclaringType()) + "."
165215
else invokePrefix = this.getArgument(-1) + "."
166216
) and
167217
args = concat(int i | i >= 0 | this.getArgument(i), ", " order by i) and
168218
result = storePrefix + invokePrefix + callable.getName() + "(" + args + ")"
169219
)
170220
}
171221

222+
/**
223+
* Returns an inline test expectation appropriate to this CSV row.
224+
*/
172225
string getExpectation() {
173226
preservesValue = true and result = "// $hasValueFlow"
174227
or
175228
preservesValue = false and result = "// $hasTaintFlow"
176229
}
177230

231+
/**
232+
* Returns a declaration and initialisation of a variable named `instance` if required; otherwise returns an empty string.
233+
*/
178234
string getInstancePrefix() {
179235
if
180236
callable instanceof Method and
@@ -187,6 +243,9 @@ class RowTestSnippet extends TRowTestSnippet {
187243
else result = ""
188244
}
189245

246+
/**
247+
* Returns the type of the output for this test.
248+
*/
190249
Type getOutputType() {
191250
if baseOutput = SummaryComponentStack::return()
192251
then result = callable.getReturnType()
@@ -197,15 +256,26 @@ class RowTestSnippet extends TRowTestSnippet {
197256
)
198257
}
199258

259+
/**
260+
* Returns the type of the input for this test.
261+
*/
200262
Type getInputType() {
201263
exists(int i |
202264
baseInput = SummaryComponentStack::argument(i) and
203265
result = getParameterType(callable, i)
204266
)
205267
}
206268

269+
/**
270+
* Returns the Java name for the type of the input to this test.
271+
*/
207272
string getInputTypeString() { result = getShortNameIfPossible(this.getInputType()) }
208273

274+
/**
275+
* Returns a call to `source()` wrapped in `newWith` methods as needed according to `input`.
276+
* For example, if the input specification is `ArrayElement of MapValue of Argument[0]`, this
277+
* will return `newWithArrayElement(newWithMapValue(source()))`.
278+
*/
209279
string getInput(SummaryComponentStack componentStack) {
210280
componentStack = input.drop(_) and
211281
(
@@ -218,6 +288,11 @@ class RowTestSnippet extends TRowTestSnippet {
218288
)
219289
}
220290

291+
/**
292+
* Returns `out` wrapped in `get` methods as needed according to `output`.
293+
* For example, if the output specification is `ArrayElement of MapValue of Argument[0]`, this
294+
* will return `getArrayElement(getMapValue(out))`.
295+
*/
221296
string getOutput(SummaryComponentStack componentStack) {
222297
componentStack = output.drop(_) and
223298
(
@@ -230,6 +305,9 @@ class RowTestSnippet extends TRowTestSnippet {
230305
)
231306
}
232307

308+
/**
309+
* 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.
310+
*/
233311
string getASupportMethod() {
234312
result =
235313
"Object newWith" + contentToken(getContent(input.drop(_).head())) +
@@ -239,6 +317,12 @@ class RowTestSnippet extends TRowTestSnippet {
239317
"(Object container) { return null; }"
240318
}
241319

320+
/**
321+
* Returns a CSV row describing a support method (`newWith` or `get` method) needed to set up the output for this test.
322+
*
323+
* For example, `newWithMapValue` will propagate a value from `Argument[0]` to `MapValue of ReturnValue`, and `getMapValue`
324+
* will do the opposite.
325+
*/
242326
string getASupportMethodModel() {
243327
exists(SummaryComponent c, string contentSsvDescription |
244328
c = input.drop(_).head() and c = Private::External::interpretComponent(contentSsvDescription)
@@ -257,6 +341,10 @@ class RowTestSnippet extends TRowTestSnippet {
257341
)
258342
}
259343

344+
/**
345+
* Gets an outer class name that this test would ideally import (and will, unless it clashes with another
346+
* type of the same name).
347+
*/
260348
Type getADesiredImport() {
261349
result =
262350
getRootSourceDeclaration([
@@ -267,6 +355,10 @@ class RowTestSnippet extends TRowTestSnippet {
267355
mayBeAmbiguous(callable) and result = getRootSourceDeclaration(callable.getAParamType())
268356
}
269357

358+
/**
359+
* Gets a test snippet (test body fragment) testing this `callable` propagates value or taint from
360+
* `input` to `output`, as specified by `row_` (which necessarily equals `row`).
361+
*/
270362
string getATestSnippetForRow(string row_) {
271363
row_ = row and
272364
result =
@@ -277,6 +369,9 @@ class RowTestSnippet extends TRowTestSnippet {
277369
}
278370
}
279371

372+
/**
373+
* Holds if type `t` does not clash with another type we want to import that has the same base name.
374+
*/
280375
predicate isImportable(Type t) {
281376
t = any(RowTestSnippet r).getADesiredImport() and
282377
t =
@@ -288,6 +383,11 @@ predicate isImportable(Type t) {
288383
)
289384
}
290385

386+
/**
387+
* Returns a printable name for type `t`, stripped of generics and, if a type variable,
388+
* replaced by its bound. Usually this is a short name, but it may be package-qualified
389+
* if we cannot import it due to a name clash.
390+
*/
291391
string getShortNameIfPossible(Type t) {
292392
getRootSourceDeclaration(t) = any(RowTestSnippet r).getADesiredImport() and
293393
if t instanceof RefType
@@ -303,6 +403,9 @@ string getShortNameIfPossible(Type t) {
303403
else result = t.getName()
304404
}
305405

406+
/**
407+
* Returns an import statement to include in the test case header.
408+
*/
306409
string getAnImportStatement() {
307410
exists(RefType t |
308411
t = any(RowTestSnippet r).getADesiredImport() and
@@ -313,14 +416,23 @@ string getAnImportStatement() {
313416
)
314417
}
315418

419+
/**
420+
* Returns a support method to include in the generated test class.
421+
*/
316422
string getASupportMethod() {
317423
result = "Object source() { return null; }" or
318424
result = "void sink(Object o) { }" or
319425
result = any(RowTestSnippet r).getASupportMethod()
320426
}
321427

428+
/**
429+
* Returns a CSV specification of the taint-/value-propagation behaviour of a test support method (`get` or `newWith` method).
430+
*/
322431
query string getASupportMethodModel() { result = any(RowTestSnippet r).getASupportMethodModel() }
323432

433+
/**
434+
* Gets a Java file body testing all `CsvRow` instances in scope against whatever classes and methods they resolve against.
435+
*/
324436
query string getTestCase() {
325437
result =
326438
"package generatedtest;\n\n" + concat(getAnImportStatement() + "\n") +

0 commit comments

Comments
 (0)