@@ -374,23 +374,51 @@ module Make<InlineExpectationsTestSig Impl> {
374374 }
375375
376376 /**
377- * Holds if `location` is the location of an inline expectation comment that this test fully
378- * owns: it carries at least one expectation, none of its expectations is ignored by this test
379- * (for example via a query ID that does not match the current query), and none is unparseable.
377+ * Holds if `location` is the location of an inline expectation comment that
378+ * `codeql test run --learn` may rewrite as a whole.
380379 *
381- * `codeql test run --learn` may rewrite such a comment as a whole. Comments that also carry an
382- * expectation this test cannot see must be left untouched, because rewriting them from only the
383- * expectations this test understands would silently drop the expectations belonging to another
384- * query that shares the same source file (for example `// $ Alert[query1] Alert[query2]`).
380+ * A comment is rewritable when it carries at least one parseable expectation, none of its
381+ * expectations is unparseable, and none mixes (in a single comma-separated group) a tag this
382+ * test understands with a tag it ignores (for example a query ID that does not match the
383+ * current query). The last condition keeps the rewrite from having to take apart a group such
384+ * as `Alert,Source[other-query]` whose parts this test treats differently; such comments are
385+ * left untouched.
386+ *
387+ * A rewritable comment may still carry whole expectations this test ignores (for example
388+ * `// $ Alert[other-query]`). Those are preserved verbatim by `getAForeignExpectation` when the
389+ * comment is rewritten, so that a `--learn` run for one query never drops an expectation that
390+ * belongs to a different query sharing the same source file.
385391 */
386- predicate isFullyOwnedComment ( Impl:: Location location ) {
392+ predicate isRewritableComment ( Impl:: Location location ) {
387393 exists ( Impl:: ExpectationComment comment | comment .getLocation ( ) = location |
388- exists ( ValidTestExpectation owned | owned .getLocation ( ) = location ) and
389- not exists ( string tags |
394+ getAnExpectation ( comment , _, _, _, _) and
395+ not exists ( InvalidTestExpectation invalid | invalid .getLocation ( ) = location ) and
396+ not exists ( string tags , string owned , string ignored |
390397 getAnExpectation ( comment , _, _, tags , _) and
391- TestImpl:: tagIsIgnored ( tags .splitAt ( "," ) )
392- ) and
393- not exists ( InvalidTestExpectation invalid | invalid .getLocation ( ) = location )
398+ owned = tags .splitAt ( "," ) and
399+ not TestImpl:: tagIsIgnored ( owned ) and
400+ ignored = tags .splitAt ( "," ) and
401+ TestImpl:: tagIsIgnored ( ignored )
402+ )
403+ )
404+ }
405+
406+ /**
407+ * Holds if the comment at `location` carries a whole expectation this test ignores (for
408+ * example one annotated with a query ID that does not match the current query), whose verbatim
409+ * text is `text` and which sits in `column` (`""` for the default column, or a named column
410+ * such as `"SPURIOUS"` / `"MISSING"`).
411+ *
412+ * `codeql test run --learn` preserves such expectations unchanged when it rewrites the comment,
413+ * because they belong to a different query that shares the same source file and this test
414+ * cannot tell whether they still hold.
415+ */
416+ predicate getAForeignExpectation ( Impl:: Location location , string column , string text ) {
417+ exists ( Impl:: ExpectationComment comment , TColumn col , string tags |
418+ comment .getLocation ( ) = location and
419+ getAnExpectation ( comment , col , text , tags , _) and
420+ column = getColumnString ( col ) and
421+ forall ( string tag | tag = tags .splitAt ( "," ) | TestImpl:: tagIsIgnored ( tag ) )
394422 )
395423 }
396424
@@ -1127,8 +1155,25 @@ module TestPostProcessing {
11271155 }
11281156
11291157 /**
1130- * Holds if `column` (`""` for the default column, or `"SPURIOUS"` / `"MISSING"`) currently
1131- * carries the expectation `text` on the comment at `commentLoc`.
1158+ * Holds if, after `--learn`, the inline expectation comment at `commentLoc` should carry the
1159+ * expectation `text` in `column` (`""` for the default column, or a named column such as
1160+ * `"SPURIOUS"` / `"MISSING"`).
1161+ *
1162+ * This combines the surviving expectations this test understands (see `learnedExpectation`)
1163+ * with the expectations it ignores (see `Test::getAForeignExpectation`), which are preserved
1164+ * verbatim so that rewriting a comment for one query never drops another query's expectation on
1165+ * the same line.
1166+ */
1167+ private predicate desiredExpectation ( TestLocation commentLoc , string column , string text ) {
1168+ learnedExpectation ( commentLoc , column , text )
1169+ or
1170+ Test:: getAForeignExpectation ( commentLoc , column , text )
1171+ }
1172+
1173+ /**
1174+ * Holds if `column` (`""` for the default column, or a named column such as `"SPURIOUS"` /
1175+ * `"MISSING"`) currently carries the expectation `text` on the comment at `commentLoc`. This
1176+ * includes expectations this test ignores, so it can be compared against `desiredExpectation`.
11321177 */
11331178 private predicate currentExpectation ( TestLocation commentLoc , string column , string text ) {
11341179 exists ( Test:: FailureLocatable e |
@@ -1140,18 +1185,20 @@ module TestPostProcessing {
11401185 or
11411186 e instanceof Test:: FalseNegativeTestExpectation and column = "MISSING"
11421187 )
1188+ or
1189+ Test:: getAForeignExpectation ( commentLoc , column , text )
11431190 }
11441191
11451192 /** Holds if `--learn` should change the set of expectations carried by the comment at `commentLoc`. */
11461193 private predicate commentNeedsRewrite ( TestLocation commentLoc ) {
11471194 exists ( string column , string text |
1148- learnedExpectation ( commentLoc , column , text ) and
1195+ desiredExpectation ( commentLoc , column , text ) and
11491196 not currentExpectation ( commentLoc , column , text )
11501197 )
11511198 or
11521199 exists ( string column , string text |
11531200 currentExpectation ( commentLoc , column , text ) and
1154- not learnedExpectation ( commentLoc , column , text )
1201+ not desiredExpectation ( commentLoc , column , text )
11551202 )
11561203 }
11571204
@@ -1171,11 +1218,11 @@ module TestPostProcessing {
11711218 * column are ordered lexically, so a rewritten comment has a deterministic layout.
11721219 */
11731220 private string renderLearnedColumn ( TestLocation commentLoc , string column ) {
1174- exists ( string text | learnedExpectation ( commentLoc , column , text ) ) and
1221+ exists ( string text | desiredExpectation ( commentLoc , column , text ) ) and
11751222 exists ( string joined |
11761223 joined =
11771224 concat ( string text |
1178- learnedExpectation ( commentLoc , column , text )
1225+ desiredExpectation ( commentLoc , column , text )
11791226 |
11801227 text , " " order by text
11811228 )
@@ -1234,11 +1281,11 @@ module TestPostProcessing {
12341281 * the surviving expectations are re-rendered. If nothing survives, the comment is deleted.
12351282 *
12361283 * The rewrite handles comments that carry several expectations across the default,
1237- * `SPURIOUS:`, and `MISSING:` columns, but only when the test *fully owns* the comment (see
1238- * `isFullyOwnedComment`), so it never discards an expectation belonging to a different query
1239- * that shares the same source file. Appending a new tag by merging it into an existing comment,
1240- * and surgically editing a comment that also carries a foreign query's expectation, are left
1241- * for a follow-up.
1284+ * `SPURIOUS:`, and `MISSING:` columns. Expectations this test ignores (for example a tag
1285+ * annotated with a different query's ID) are preserved verbatim, so the comment keeps any
1286+ * expectation belonging to a different query that shares the same source file; see
1287+ * `isRewritableComment` and `Test::getAForeignExpectation`. Appending a new tag by merging it
1288+ * into an existing comment rather than adding a separate one is left for a follow-up.
12421289 */
12431290 query predicate learnEdits (
12441291 string file , int line , string operation , int startColumn , int endColumn , string text
@@ -1270,13 +1317,13 @@ module TestPostProcessing {
12701317 // This subsumes the single-expectation removal and MISSING-promotion cases and additionally
12711318 // handles comments that carry several expectations across the default, `SPURIOUS:`, and
12721319 // `MISSING:` columns. The comment is replaced from its marker to the end of the line: with
1273- // the re-rendered surviving expectations, or with the empty string when nothing survives (in
1320+ // the re-rendered desired expectations, or with the empty string when none remains (in
12741321 // which case `endColumn = 0` also trims the whitespace gap the removed comment leaves
12751322 // behind). `endColumn = 0` is the engine's "to end of line" convention, which avoids
12761323 // depending on how each extractor reports a line comment's end column (e.g. Swift reports it
12771324 // as ending at column 1 of the next line).
12781325 exists ( TestLocation commentLoc , string relativePath , int sl , int sc |
1279- Test:: isFullyOwnedComment ( commentLoc ) and
1326+ Test:: isRewritableComment ( commentLoc ) and
12801327 commentNeedsRewrite ( commentLoc ) and
12811328 parseLocationString ( commentLoc .getRelativeUrl ( ) , relativePath , sl , sc , _, _) and
12821329 file = relativePath and
@@ -1285,10 +1332,10 @@ module TestPostProcessing {
12851332 startColumn = sc and
12861333 endColumn = 0 and
12871334 (
1288- exists ( string column , string t | learnedExpectation ( commentLoc , column , t ) ) and
1335+ exists ( string column , string t | desiredExpectation ( commentLoc , column , t ) ) and
12891336 text = renderLearnedComment ( relativePath , commentLoc )
12901337 or
1291- not exists ( string column , string t | learnedExpectation ( commentLoc , column , t ) ) and
1338+ not exists ( string column , string t | desiredExpectation ( commentLoc , column , t ) ) and
12921339 text = ""
12931340 )
12941341 )
0 commit comments