Skip to content

Commit e7a3e6b

Browse files
committed
Shared: Post-processing query for inline test expectations
1 parent b111194 commit e7a3e6b

File tree

1 file changed

+309
-9
lines changed

1 file changed

+309
-9
lines changed

shared/util/codeql/util/test/InlineExpectationsTest.qll

Lines changed: 309 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,31 @@ module Make<InlineExpectationsTestSig Impl> {
134134
* predicate for an active test will be ignored. This makes it possible to write multiple tests in
135135
* different `.ql` files that all query the same source code.
136136
*/
137+
bindingset[result]
137138
string getARelevantTag();
138139

140+
/**
141+
* Holds if expected tag `expectedTag` matches actual tag `actualTag`.
142+
*
143+
* This is normally defined as `expectedTag = actualTag`.
144+
*/
145+
bindingset[expectedTag, actualTag]
146+
default predicate tagMatches(string expectedTag, string actualTag) { expectedTag = actualTag }
147+
148+
/** Holds if expectations marked with `expectedTag` are optional. */
149+
bindingset[expectedTag]
150+
default predicate tagIsOptional(string expectedTag) { none() }
151+
152+
/**
153+
* Holds if expected value `expectedValue` matches actual value `actualValue`.
154+
*
155+
* This is normally defined as `expectedValue = actualValue`.
156+
*/
157+
bindingset[expectedValue, actualValue]
158+
default predicate valueMatches(string expectedValue, string actualValue) {
159+
expectedValue = actualValue
160+
}
161+
139162
/**
140163
* Returns the actual results of the query that is being tested. Each result consist of the
141164
* following values:
@@ -200,13 +223,13 @@ module Make<InlineExpectationsTestSig Impl> {
200223
not exists(ActualTestResult actualResult | expectation.matchesActualResult(actualResult)) and
201224
expectation.getTag() = TestImpl::getARelevantTag() and
202225
element = expectation and
203-
(
204-
expectation instanceof GoodTestExpectation and
205-
message = "Missing result: " + expectation.getExpectationText()
206-
or
207-
expectation instanceof FalsePositiveTestExpectation and
208-
message = "Fixed spurious result: " + expectation.getExpectationText()
209-
)
226+
not expectation.isOptional()
227+
|
228+
expectation instanceof GoodTestExpectation and
229+
message = "Missing result: " + expectation.getExpectationText()
230+
or
231+
expectation instanceof FalsePositiveTestExpectation and
232+
message = "Fixed spurious result: " + expectation.getExpectationText()
210233
)
211234
or
212235
exists(InvalidTestExpectation expectation |
@@ -311,9 +334,11 @@ module Make<InlineExpectationsTestSig Impl> {
311334

312335
predicate matchesActualResult(ActualTestResult actualResult) {
313336
onSameLine(pragma[only_bind_into](this), actualResult) and
314-
this.getTag() = actualResult.getTag() and
315-
this.getValue() = actualResult.getValue()
337+
TestImpl::tagMatches(this.getTag(), actualResult.getTag()) and
338+
TestImpl::valueMatches(this.getValue(), actualResult.getValue())
316339
}
340+
341+
predicate isOptional() { TestImpl::tagIsOptional(tag) }
317342
}
318343

319344
// Note: These next three classes correspond to all the possible values of type `TColumn`.
@@ -337,6 +362,18 @@ module Make<InlineExpectationsTestSig Impl> {
337362
string getExpectation() { result = expectation }
338363
}
339364

365+
/**
366+
* Gets a test expectation that matches the actual result at the given location.
367+
*/
368+
ValidTestExpectation getAMatchingExpectation(
369+
Impl::Location location, string element, string tag, string val, boolean optional
370+
) {
371+
exists(ActualTestResult actualResult |
372+
result.matchesActualResult(actualResult) and
373+
actualResult = TActualResult(location, element, tag, val, optional)
374+
)
375+
}
376+
340377
query predicate testFailures(FailureLocatable element, string message) {
341378
hasFailureMessage(element, message)
342379
}
@@ -385,6 +422,7 @@ module Make<InlineExpectationsTestSig Impl> {
385422
* ```
386423
*/
387424
module MergeTests<TestSig TestImpl1, TestSig TestImpl2> implements TestSig {
425+
bindingset[result]
388426
string getARelevantTag() {
389427
result = TestImpl1::getARelevantTag() or result = TestImpl2::getARelevantTag()
390428
}
@@ -408,6 +446,7 @@ module Make<InlineExpectationsTestSig Impl> {
408446
module MergeTests3<TestSig TestImpl1, TestSig TestImpl2, TestSig TestImpl3> implements TestSig {
409447
private module M = MergeTests<MergeTests<TestImpl1, TestImpl2>, TestImpl3>;
410448

449+
bindingset[result]
411450
string getARelevantTag() { result = M::getARelevantTag() }
412451

413452
predicate hasActualResult(Impl::Location location, string element, string tag, string value) {
@@ -427,6 +466,7 @@ module Make<InlineExpectationsTestSig Impl> {
427466
{
428467
private module M = MergeTests<MergeTests3<TestImpl1, TestImpl2, TestImpl3>, TestImpl4>;
429468

469+
bindingset[result]
430470
string getARelevantTag() { result = M::getARelevantTag() }
431471

432472
predicate hasActualResult(Impl::Location location, string element, string tag, string value) {
@@ -448,6 +488,7 @@ module Make<InlineExpectationsTestSig Impl> {
448488
private module M =
449489
MergeTests<MergeTests4<TestImpl1, TestImpl2, TestImpl3, TestImpl4>, TestImpl5>;
450490

491+
bindingset[result]
451492
string getARelevantTag() { result = M::getARelevantTag() }
452493

453494
predicate hasActualResult(Impl::Location location, string element, string tag, string value) {
@@ -590,3 +631,262 @@ private string expectationPattern() {
590631
result = tags + "(?:=" + value + ")?"
591632
)
592633
}
634+
635+
/**
636+
* Provides logic for creating a `@kind test-postprocess` query that checks
637+
* inline test expectations using `$ Alert` markers.
638+
*
639+
* The postprocessing query works for queries of kind `problem` and `path-problem`,
640+
* and each query result must have a matching `$ Alert` comment. It is possible to
641+
* augment the comment with a query ID, in order to support cases where multiple
642+
* `.qlref` tests share the same test code:
643+
*
644+
* ```rust
645+
* var x = ""; // $ Alert[rust/unused-value]
646+
* return;
647+
* foo(); // $ Alert[rust/unreachable-code]
648+
* ```
649+
*
650+
* In the example above, the `$ Alert[rust/unused-value]` commment is only taken
651+
* into account in the test for the query with ID `rust/unused-value`, and vice
652+
* versa for the `$ Alert[rust/unreachable-code]` comment.
653+
*
654+
* For `path-problem` queries, each source and sink must additionally be annotated
655+
* (`$ Source` and `$ Sink`, respectively), except when their location coincides
656+
* with the location of the alert itself, in which case only `$ Alert` is needed.
657+
*
658+
* Example:
659+
*
660+
* ```csharp
661+
* var queryParam = Request.QueryString["param"]; // $ Source
662+
* Write(Html.Raw(queryParam)); // $ Alert
663+
* ```
664+
*
665+
* Morover, it is possible to tag sources with a unique identifier:
666+
*
667+
* ```csharp
668+
* var queryParam = Request.QueryString["param"]; // $ Source=source1
669+
* Write(Html.Raw(queryParam)); // $ Alert=source1
670+
* ```
671+
*
672+
* In this case, the source and sink must have the same tag in order
673+
* to be matched.
674+
*/
675+
module TestPostProcessing {
676+
external predicate queryResults(string relation, int row, int column, string data);
677+
678+
external predicate queryRelations(string relation);
679+
680+
external predicate queryMetadata(string key, string value);
681+
682+
private string getQueryId() { queryMetadata("id", result) }
683+
684+
private string getQueryKind() { queryMetadata("kind", result) }
685+
686+
signature module InputSig<InlineExpectationsTestSig Input> {
687+
string getRelativeUrl(Input::Location location);
688+
}
689+
690+
module Make<InlineExpectationsTestSig Input, InputSig<Input> Input2> {
691+
private import InlineExpectationsTest as InlineExpectationsTest
692+
private import InlineExpectationsTest::Make<Input>
693+
694+
/**
695+
* Gets the tag to be used for the path-problem source at result row `row`.
696+
*
697+
* This is either `Source` or `Alert`, depending on whether the location
698+
* of the source matches the location of the alert.
699+
*/
700+
private string getSourceTag(int row) {
701+
getQueryKind() = "path-problem" and
702+
exists(string loc | queryResults("#select", row, 2, loc) |
703+
if queryResults("#select", row, 0, loc) then result = "Alert" else result = "Source"
704+
)
705+
}
706+
707+
/**
708+
* Gets the tag to be used for the path-problem sink at result row `row`.
709+
*
710+
* This is either `Sink` or `Alert`, depending on whether the location
711+
* of the sink matches the location of the alert.
712+
*/
713+
private string getSinkTag(int row) {
714+
getQueryKind() = "path-problem" and
715+
exists(string loc | queryResults("#select", row, 4, loc) |
716+
if queryResults("#select", row, 0, loc) then result = "Alert" else result = "Sink"
717+
)
718+
}
719+
720+
/**
721+
* A configuration for matching `// $ Source=foo` comments against actual
722+
* path-problem sources.
723+
*/
724+
private module PathProblemSourceTestInput implements TestSig {
725+
string getARelevantTag() { result = getSourceTag(_) }
726+
727+
bindingset[expectedValue, actualValue]
728+
predicate valueMatches(string expectedValue, string actualValue) {
729+
exists(expectedValue) and
730+
actualValue = ""
731+
}
732+
733+
additional predicate hasPathProblemSource(
734+
int row, Input::Location location, string element, string tag, string value
735+
) {
736+
getQueryKind() = "path-problem" and
737+
exists(string loc |
738+
queryResults("#select", row, 2, loc) and
739+
queryResults("#select", row, 3, element) and
740+
tag = getSourceTag(row) and
741+
value = "" and
742+
Input2::getRelativeUrl(location) = loc
743+
)
744+
}
745+
746+
predicate hasActualResult(Input::Location location, string element, string tag, string value) {
747+
hasPathProblemSource(_, location, element, tag, value)
748+
}
749+
}
750+
751+
private module PathProblemSourceTest = MakeTest<PathProblemSourceTestInput>;
752+
753+
private module TestInput implements TestSig {
754+
bindingset[result]
755+
string getARelevantTag() { any() }
756+
757+
private string getTagRegex() {
758+
exists(string sourceSinkTags |
759+
getQueryKind() = "problem" and
760+
sourceSinkTags = ""
761+
or
762+
sourceSinkTags = "|" + getSourceTag(_) + "|" + getSinkTag(_)
763+
|
764+
result = "(Alert" + sourceSinkTags + ")(\\[(.*)\\])?"
765+
)
766+
}
767+
768+
bindingset[expectedTag, actualTag]
769+
predicate tagMatches(string expectedTag, string actualTag) {
770+
actualTag = expectedTag.regexpCapture(getTagRegex(), 1) and
771+
(
772+
// expected tag is annotated with a query ID
773+
getQueryId() = expectedTag.regexpCapture(getTagRegex(), 3)
774+
or
775+
// expected tag is not annotated with a query ID
776+
not exists(expectedTag.regexpCapture(getTagRegex(), 3))
777+
)
778+
}
779+
780+
bindingset[expectedTag]
781+
predicate tagIsOptional(string expectedTag) {
782+
// ignore irrelevant tags
783+
not expectedTag.regexpMatch(getTagRegex())
784+
or
785+
// ignore tags annotated with a query ID that does not match the current query ID
786+
exists(string queryId |
787+
queryId = expectedTag.regexpCapture(getTagRegex(), 3) and
788+
queryId != getQueryId()
789+
)
790+
}
791+
792+
bindingset[expectedValue, actualValue]
793+
predicate valueMatches(string expectedValue, string actualValue) {
794+
expectedValue = actualValue
795+
or
796+
actualValue = ""
797+
}
798+
799+
private predicate hasPathProblemSource = PathProblemSourceTestInput::hasPathProblemSource/5;
800+
801+
/**
802+
* Gets the expected sink value for result row `row`. This value must
803+
* match the value at the corresponding path-problem source (if it is
804+
* present).
805+
*/
806+
private string getSinkValue(int row) {
807+
exists(Input::Location location, string element, string tag, string val |
808+
hasPathProblemSource(row, location, element, tag, val) and
809+
result =
810+
PathProblemSourceTest::getAMatchingExpectation(location, element, tag, val, false)
811+
.getValue()
812+
)
813+
}
814+
815+
private predicate hasPathProblemSink(
816+
int row, Input::Location location, string element, string tag, string value
817+
) {
818+
getQueryKind() = "path-problem" and
819+
exists(string loc |
820+
queryResults("#select", row, 4, loc) and
821+
queryResults("#select", row, 5, element) and
822+
tag = getSinkTag(row) and
823+
Input2::getRelativeUrl(location) = loc
824+
|
825+
not exists(getSinkValue(row)) and value = ""
826+
or
827+
value = getSinkValue(row)
828+
)
829+
}
830+
831+
private predicate hasAlert(Input::Location location, string element, string tag, string value) {
832+
getQueryKind() = ["problem", "path-problem"] and
833+
exists(int row, string loc |
834+
queryResults("#select", row, 0, loc) and
835+
queryResults("#select", row, 2, element) and
836+
tag = "Alert" and
837+
value = "" and
838+
Input2::getRelativeUrl(location) = loc and
839+
not hasPathProblemSource(row, location, _, _, _) and
840+
not hasPathProblemSink(row, location, _, _, _)
841+
)
842+
}
843+
844+
predicate hasActualResult(Input::Location location, string element, string tag, string value) {
845+
hasPathProblemSource(_, location, element, tag, value)
846+
or
847+
hasPathProblemSink(_, location, element, tag, value)
848+
or
849+
hasAlert(location, element, tag, value)
850+
}
851+
}
852+
853+
private module Test = MakeTest<TestInput>;
854+
855+
private newtype TTestFailure =
856+
MkTestFailure(Test::FailureLocatable f, string message) { Test::testFailures(f, message) }
857+
858+
private predicate rankedTestFailures(int i, MkTestFailure f) {
859+
f =
860+
rank[i](MkTestFailure f0, Test::FailureLocatable fl, string message, string filename,
861+
int startLine, int startColumn, int endLine, int endColumn |
862+
f0 = MkTestFailure(fl, message) and
863+
fl.getLocation().hasLocationInfo(filename, startLine, startColumn, endLine, endColumn)
864+
|
865+
f0 order by filename, startLine, startColumn, endLine, endColumn, message
866+
)
867+
}
868+
869+
query predicate results(string relation, int row, int column, string data) {
870+
queryResults(relation, row, column, data)
871+
or
872+
exists(MkTestFailure f, Test::FailureLocatable fl, string message |
873+
relation = "testFailures" and
874+
rankedTestFailures(row, f) and
875+
f = MkTestFailure(fl, message)
876+
|
877+
column = 0 and data = Input2::getRelativeUrl(fl.getLocation())
878+
or
879+
column = 1 and data = fl.toString()
880+
or
881+
column = 2 and data = message
882+
)
883+
}
884+
885+
query predicate resultRelations(string relation) {
886+
queryRelations(relation)
887+
or
888+
Test::testFailures(_, _) and
889+
relation = "testFailures"
890+
}
891+
}
892+
}

0 commit comments

Comments
 (0)