@@ -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