summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-08-13 11:03:38 +0200
committerDanilo M. <danix@danix.xyz>2026-08-13 11:03:38 +0200
commitfe5703419f2ac2a5e619b3d527530a71a9a9499e (patch)
treefc7a588c46960643f80fee0a684465417415763a
parent043b150f7651c598dac8fc02d7068b940d0f8741 (diff)
downloadqtmaildir-fe5703419f2ac2a5e619b3d527530a71a9a9499e.tar.gz
qtmaildir-fe5703419f2ac2a5e619b3d527530a71a9a9499e.zip
docs,rulequery: state the tag quoting rule rather than the test
The draft compile() quoted every Is/IsNot term, which contradicted the same task's own assertion that a negated tag compiles to . The implementer resolved it in the direction the tests specify, and the resolution is right: notmuch reads tag:inbox and tag:"inbox" identically, counting 5322 either way against the live index, so quoting a tag would change the stored string without changing what it matches. That breaks the byte-for-byte round trip this type exists to guarantee. Restate the comment as the rule rather than as a note about what a test expects, correct the plan's draft so the remaining tasks do not inherit the contradiction, and warn the parser task that a quoted tag must not be read back as a quoting operator.
-rw-r--r--docs/superpowers/plans/2026-08-13-rule-builder.md18
-rw-r--r--src/rulequery.cpp11
2 files changed, 23 insertions, 6 deletions
diff --git a/docs/superpowers/plans/2026-08-13-rule-builder.md b/docs/superpowers/plans/2026-08-13-rule-builder.md
index fb39cca..4f65f7d 100644
--- a/docs/superpowers/plans/2026-08-13-rule-builder.md
+++ b/docs/superpowers/plans/2026-08-13-rule-builder.md
@@ -376,9 +376,20 @@ bool needsQuotes(const RuleTerm &term)
{
if (term.field == RuleTerm::Folder)
return true;
- if (term.op == RuleTerm::Is || term.op == RuleTerm::IsNot)
+ if (term.value.contains(QLatin1Char(' ')))
return true;
- return term.value.contains(QLatin1Char(' '));
+ // Is/IsNot means an exact phrase, and only the free-text fields need
+ // quotes to express one. A tag or an attachment name is a single bare
+ // token to notmuch, which reads `tag:inbox` and `tag:"inbox"` identically
+ // (both count 5322 against the live index). Quoting them would therefore
+ // change the stored string without changing what it matches, and this
+ // type's whole contract is that an unedited rule compiles back byte for
+ // byte.
+ if (term.op == RuleTerm::Is || term.op == RuleTerm::IsNot) {
+ return term.field == RuleTerm::From || term.field == RuleTerm::To
+ || term.field == RuleTerm::Cc || term.field == RuleTerm::Subject;
+ }
+ return false;
}
QString compileTerm(const RuleTerm &term)
@@ -793,6 +804,9 @@ bool parseTerm(const QString &token, RuleTerm *out)
return !value.isEmpty();
}
+ // Tag and Attachment compile unquoted (see needsQuotes in Task 2), so
+ // their operator must not be inferred from the quoting: reading a quoted
+ // tag back as Is would compile it unquoted and change the stored string.
if (field == RuleTerm::Attachment)
out->op = RuleTerm::Has;
else if (field == RuleTerm::Tag)
diff --git a/src/rulequery.cpp b/src/rulequery.cpp
index 198c583..9b79b2b 100644
--- a/src/rulequery.cpp
+++ b/src/rulequery.cpp
@@ -51,10 +51,13 @@ bool needsQuotes(const RuleTerm &term)
return true;
if (term.value.contains(QLatin1Char(' ')))
return true;
- // Is/IsNot asks for an exact phrase, which only free-text fields need
- // quoting for: tag: and attachment: values are already bare words, and
- // quoting them changes nothing notmuch cares about but breaks the test's
- // documented expectation of an unquoted tag.
+ // Is/IsNot means an exact phrase, and only the free-text fields need
+ // quotes to express one. A tag or an attachment name is a single bare
+ // token to notmuch, which reads `tag:inbox` and `tag:"inbox"` identically
+ // (both count 5322 against the live index). Quoting them would therefore
+ // change the stored string without changing what it matches, and this
+ // type's whole contract is that an unedited rule compiles back byte for
+ // byte.
if (term.op == RuleTerm::Is || term.op == RuleTerm::IsNot) {
return term.field == RuleTerm::From || term.field == RuleTerm::To
|| term.field == RuleTerm::Cc || term.field == RuleTerm::Subject;