diff options
| -rw-r--r-- | src/rulequery.cpp | 81 | ||||
| -rw-r--r-- | tests/test_rulequery.cpp | 24 |
2 files changed, 105 insertions, 0 deletions
diff --git a/src/rulequery.cpp b/src/rulequery.cpp index d4d8966..31996fa 100644 --- a/src/rulequery.cpp +++ b/src/rulequery.cpp @@ -215,6 +215,35 @@ bool parseTerm(const QString &token, RuleTerm *out) return true; } +/// Splits `(A or B) and not C and not D` into its group and its remainder. +/// Returns false when the query does not start with a balanced group. +bool splitLeadingGroup(const QString &query, QString *group, QString *rest) +{ + if (!query.startsWith(QLatin1Char('('))) + return false; + + int depth = 0; + bool inQuotes = false; + for (int i = 0; i < query.size(); ++i) { + const QChar c = query.at(i); + if (c == QLatin1Char('"')) + inQuotes = !inQuotes; + if (inQuotes) + continue; + if (c == QLatin1Char('(')) + ++depth; + else if (c == QLatin1Char(')')) { + --depth; + if (depth == 0) { + *group = query.mid(1, i - 1).trimmed(); + *rest = query.mid(i + 1).trimmed(); + return true; + } + } + } + return false; +} + } // namespace bool operator==(const RuleTerm &a, const RuleTerm &b) @@ -268,6 +297,58 @@ RuleQuery RuleQuery::parse(const QString &query) return out; } + QString group; + QString rest; + if (splitLeadingGroup(trimmed, &group, &rest)) { + // Only one nested shape is representable: an `or` group followed by + // `and not` exclusions. Anything else rejects whole. + if (group.contains(QLatin1Char('('))) + return RuleQuery(); + + const RuleQuery inner = parse(group); + if (!inner.parsed || inner.join != Any || !inner.exclusions.isEmpty()) + return RuleQuery(); + + out.join = Any; + out.terms = inner.terms; + + if (rest.isEmpty()) { + // A group with nothing after it compiles back WITHOUT parens, + // since compile() only adds them when exclusions follow. Round + // trip would break, so this is not representable. + return RuleQuery(); + } + + // The remainder must be nothing but `and not <term>` repetitions. + QStringList tail; + if (!tokenise(rest, &tail)) + return RuleQuery(); + + int i = 0; + while (i < tail.size()) { + if (tail.at(i).compare(QStringLiteral("and"), + Qt::CaseInsensitive) != 0) + return RuleQuery(); + ++i; + if (i >= tail.size() + || tail.at(i).compare(QStringLiteral("not"), + Qt::CaseInsensitive) != 0) + return RuleQuery(); + ++i; + if (i >= tail.size()) + return RuleQuery(); + + RuleTerm term; + if (!parseTerm(tail.at(i), &term)) + return RuleQuery(); + out.exclusions.append(term); + ++i; + } + + out.parsed = true; + return out; + } + QStringList tokens; if (!tokenise(trimmed, &tokens)) return RuleQuery(); diff --git a/tests/test_rulequery.cpp b/tests/test_rulequery.cpp index b8d6f22..88485f3 100644 --- a/tests/test_rulequery.cpp +++ b/tests/test_rulequery.cpp @@ -47,6 +47,7 @@ private slots: void quotedValuesLoseTheirQuotes(); void aNegatedTermParsesAsANegatedOperator(); void whatParsesCompilesBackUnchanged(); + void anOrGroupWithExclusionsParses(); }; void TestRuleQuery::aSingleContainsTermCompiles() @@ -291,6 +292,8 @@ void TestRuleQuery::whatParsesCompilesBackUnchanged() QStringLiteral("from:vendor.example.org and not tag:inbox"), QStringLiteral("from:vendor.example.org and not subject:receipt " "and not subject:refund"), + QStringLiteral("(from:one.example.org or from:two.example.org) " + "and not subject:receipt"), }; for (const QString &query : queries) { @@ -300,5 +303,26 @@ void TestRuleQuery::whatParsesCompilesBackUnchanged() } } +void TestRuleQuery::anOrGroupWithExclusionsParses() +{ + const RuleQuery q = RuleQuery::parse( + QStringLiteral("(from:vendor.example.org or from:vendor.example.net) " + "and not subject:receipt and not subject:refund")); + + QVERIFY(q.parsed); + QCOMPARE(q.join, RuleQuery::Any); + QCOMPARE(q.terms.size(), 2); + QCOMPARE(q.exclusions.size(), 2); + QCOMPARE(q.exclusions.at(0).field, RuleTerm::Subject); + QCOMPARE(q.exclusions.at(0).op, RuleTerm::Contains); + QCOMPARE(q.exclusions.at(0).value, QStringLiteral("receipt")); + + // The round trip is the point: this must come back as it went in. + QCOMPARE(q.compile(), + QStringLiteral("(from:vendor.example.org or " + "from:vendor.example.net) " + "and not subject:receipt and not subject:refund")); +} + QTEST_MAIN(TestRuleQuery) #include "test_rulequery.moc" |
