@@ -1154,6 +1154,40 @@ module TestPostProcessing {
11541154 )
11551155 }
11561156
1157+ /**
1158+ * Holds if some non-optional `Alert` result with no matching expectation fires on `line` of
1159+ * `relativePath` -- an *unexpected result* that `--learn` should record with a new `Alert`
1160+ * expectation, either by appending a fresh comment or by merging into an existing one.
1161+ */
1162+ private predicate hasUnexpectedAlertOnLine ( string relativePath , int line ) {
1163+ exists ( Test:: ActualTestResult actualResult |
1164+ actualResult .getTag ( ) = "Alert" and
1165+ actualResult .getValue ( ) = "" and
1166+ not actualResult .isOptional ( ) and
1167+ not exists (
1168+ Test:: getAMatchingExpectation ( actualResult .getLocation ( ) , actualResult .toString ( ) ,
1169+ actualResult .getTag ( ) , actualResult .getValue ( ) , false )
1170+ ) and
1171+ parseLocationString ( actualResult .getLocation ( ) .getRelativeUrl ( ) , relativePath , _, _, line , _)
1172+ )
1173+ }
1174+
1175+ /**
1176+ * Holds if `--learn` should merge a freshly learned `Alert` expectation into the existing,
1177+ * rewritable comment at `commentLoc` (in `column` `""`, the default), because an unexpected
1178+ * `Alert` result fires on that comment's line. Merging keeps the new tag alongside the
1179+ * comment's existing expectations rather than appending a second comment to the line.
1180+ */
1181+ private predicate mergedNewTag ( TestLocation commentLoc , string column , string text ) {
1182+ exists ( string relativePath , int line |
1183+ Test:: isRewritableComment ( commentLoc ) and
1184+ parseLocationString ( commentLoc .getRelativeUrl ( ) , relativePath , line , _, _, _) and
1185+ hasUnexpectedAlertOnLine ( relativePath , line ) and
1186+ column = "" and
1187+ text = "Alert"
1188+ )
1189+ }
1190+
11571191 /**
11581192 * Holds if, after `--learn`, the inline expectation comment at `commentLoc` should carry the
11591193 * expectation `text` in `column` (`""` for the default column, or a named column such as
@@ -1162,12 +1196,14 @@ module TestPostProcessing {
11621196 * This combines the surviving expectations this test understands (see `learnedExpectation`)
11631197 * with the expectations it ignores (see `Test::getAForeignExpectation`), which are preserved
11641198 * verbatim so that rewriting a comment for one query never drops another query's expectation on
1165- * the same line.
1199+ * the same line, and with any freshly learned tag merged into the comment (see `mergedNewTag`) .
11661200 */
11671201 private predicate desiredExpectation ( TestLocation commentLoc , string column , string text ) {
11681202 learnedExpectation ( commentLoc , column , text )
11691203 or
11701204 Test:: getAForeignExpectation ( commentLoc , column , text )
1205+ or
1206+ mergedNewTag ( commentLoc , column , text )
11711207 }
11721208
11731209 /**
@@ -1272,29 +1308,32 @@ module TestPostProcessing {
12721308 *
12731309 * The following edits are emitted:
12741310 *
1275- * - an actual result with no matching expectation gets a new `// $ Alert` comment appended
1276- * (an *unexpected result*); and
1277- * - an existing expectation comment that this test fully owns is rewritten as a whole so that
1278- * it matches the current results: obsolete default and `// $ SPURIOUS:` expectations are
1279- * dropped (a *missing result* or a *fixed spurious result*), a `// $ MISSING:` expectation
1280- * whose result now fires is promoted to the default column (a *fixed missing result*), and
1281- * the surviving expectations are re-rendered. If nothing survives, the comment is deleted.
1311+ * - an actual result with no matching expectation records a new `Alert` expectation (an
1312+ * *unexpected result*): if the result's line already has a rewritable comment the tag is
1313+ * merged into it (see below), otherwise a fresh `// $ Alert` comment is appended; and
1314+ * - an existing rewritable expectation comment is rewritten as a whole so that it matches the
1315+ * current results: obsolete default and `// $ SPURIOUS:` expectations are dropped (a *missing
1316+ * result* or a *fixed spurious result*), a `// $ MISSING:` expectation whose result now fires
1317+ * is promoted to the default column (a *fixed missing result*), a freshly learned tag on the
1318+ * line is merged in, and the resulting expectations are re-rendered. If nothing remains, the
1319+ * comment is deleted.
12821320 *
12831321 * The rewrite handles comments that carry several expectations across the default,
12841322 * `SPURIOUS:`, and `MISSING:` columns. Expectations this test ignores (for example a tag
12851323 * annotated with a different query's ID) are preserved verbatim, so the comment keeps any
12861324 * 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.
1325+ * `isRewritableComment` and `Test::getAForeignExpectation`.
12891326 */
12901327 query predicate learnEdits (
12911328 string file , int line , string operation , int startColumn , int endColumn , string text
12921329 ) {
1293- // Unexpected result: append a new `// $ Alert` comment on the alert's line. The comment
1294- // must go on the result's *end* line, because an expectation matches a result when the
1295- // expectation's start line equals the result's end line (see `onSameLine`). For most
1296- // languages a result spans a single line, but some (e.g. Rust) include leading trivia in
1297- // the location, so the start and end lines differ.
1330+ // Unexpected result with no comment to merge into: append a new `// $ Alert` comment on the
1331+ // alert's line. The comment must go on the result's *end* line, because an expectation
1332+ // matches a result when the expectation's start line equals the result's end line (see
1333+ // `onSameLine`). For most languages a result spans a single line, but some (e.g. Rust)
1334+ // include leading trivia in the location, so the start and end lines differ. If the line
1335+ // already has a rewritable comment, the tag is merged into it by the rewrite disjunct below
1336+ // (see `mergedNewTag`) rather than appended as a separate comment.
12981337 exists ( Test:: ActualTestResult actualResult , string relativePath , int el , string comment |
12991338 actualResult .getTag ( ) = "Alert" and
13001339 actualResult .getValue ( ) = "" and
@@ -1304,6 +1343,10 @@ module TestPostProcessing {
13041343 actualResult .getTag ( ) , actualResult .getValue ( ) , false )
13051344 ) and
13061345 parseLocationString ( actualResult .getLocation ( ) .getRelativeUrl ( ) , relativePath , _, _, el , _) and
1346+ not exists ( TestLocation existing |
1347+ Test:: isRewritableComment ( existing ) and
1348+ parseLocationString ( existing .getRelativeUrl ( ) , relativePath , el , _, _, _)
1349+ ) and
13071350 comment = renderExpectationComment ( relativePath , "Alert" ) and
13081351 file = relativePath and
13091352 line = el and
0 commit comments