MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 277 / 0 / 277
Functions: 100.0% 17 / 0 / 17
Branches: 92.0% 675 / 0 / 734

app/SearchQuery.cpp
Line Branch Exec Source
1 #include "SearchQuery.h"
2
3 #include <QDate>
4 #include <QLocale>
5 #include <QDateTime>
6 #include <QStringList>
7
8 using Tri = MailCache::SearchFilter::Tri;
9
10 namespace {
11
12 // Split the input into tokens, honoring double quotes so a prefix value can
13 // contain spaces (subject:"offene Rechnung" → one token "subject:offene
14 // Rechnung"). Quote characters are dropped; spaces inside quotes are kept.
15 // `quoted`: the token held a quote after its start (prefix:"value").
16 struct QueryToken { QString text; bool literal = false; bool quoted = false; };
17 14837 QList<QueryToken> tokenize(const QString &input) {
18 14837 QList<QueryToken> tokens;
19 14837 QString cur;
20 14837 bool inQuotes = false;
21 14837 bool hasToken = false;
22 14837 bool literal = false;
23 14837 bool quoted = false;
24
2/2
✓ Branch 53 → 3 taken 213158 times.
✓ Branch 53 → 54 taken 14837 times.
227995 for (qsizetype index = 0; index < input.size(); ++index) {
25 213158 const QChar ch = input.at(index);
26
7/8
✓ Branch 4 → 5 taken 292 times.
✓ Branch 4 → 22 taken 212866 times.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 22 taken 289 times.
✓ Branch 10 → 11 taken 3 times.
✗ Branch 10 → 22 not taken.
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 21 taken 1 time.
213163 if (inQuotes && ch == QLatin1Char('\\') && index + 1 < input.size() &&
27
3/4
✓ Branch 20 → 21 taken 2 times.
✗ Branch 20 → 22 not taken.
✓ Branch 23 → 24 taken 3 times.
✓ Branch 23 → 27 taken 213155 times.
213163 (input.at(index + 1) == QLatin1Char('\\') || input.at(index + 1) == QLatin1Char('"'))) {
28
1/1
✓ Branch 25 → 26 taken 3 times.
3678 cur.append(input.at(++index)); hasToken = true; continue;
29 }
30
2/2
✓ Branch 30 → 31 taken 55 times.
✓ Branch 30 → 35 taken 213100 times.
213155 if (ch == QLatin1Char('"')) {
31
3/4
✓ Branch 31 → 32 taken 7 times.
✓ Branch 31 → 34 taken 48 times.
✓ Branch 32 → 33 taken 7 times.
✗ Branch 32 → 34 not taken.
55 if (!hasToken && !inQuotes) literal = true;
32 55 quoted = true;
33 55 inQuotes = !inQuotes;
34 55 hasToken = true; // an empty quoted string is still a (empty) token start
35 55 continue;
36 }
37
6/6
✓ Branch 36 → 37 taken 3648 times.
✓ Branch 36 → 39 taken 209452 times.
✓ Branch 37 → 38 taken 3620 times.
✓ Branch 37 → 39 taken 28 times.
✓ Branch 40 → 41 taken 3620 times.
✓ Branch 40 → 48 taken 209480 times.
213100 if (ch.isSpace() && !inQuotes) {
38
2/2
✓ Branch 41 → 42 taken 3613 times.
✓ Branch 41 → 47 taken 7 times.
3620 if (hasToken) {
39 3613 tokens.append({cur, literal, quoted});
40 3613 cur.clear();
41 3613 hasToken = false;
42 3613 literal = false;
43 3613 quoted = false;
44 }
45 3620 continue;
46 }
47
1/1
✓ Branch 48 → 49 taken 209480 times.
209480 cur.append(ch);
48 209480 hasToken = true;
49 }
50
2/2
✓ Branch 54 → 55 taken 14497 times.
✓ Branch 54 → 59 taken 340 times.
14837 if (hasToken)
51 14497 tokens.append({cur, literal, quoted});
52 14837 return tokens;
53 32947 }
54
55 // The English preset word for a relative date value, or "" when the value
56 // is no preset. German words are keywords (SearchVocabulary.h) and map to
57 // their English word so the canonical query is language-independent.
58 11602 QString canonicalDatePreset(const QString &val) {
59
12/14
✓ Branch 6 → 7 taken 11596 times.
✓ Branch 6 → 12 taken 6 times.
✓ Branch 11 → 12 taken 2 times.
✓ Branch 11 → 13 taken 11594 times.
✓ Branch 14 → 15 taken 11596 times.
✓ Branch 14 → 16 taken 6 times.
✓ Branch 16 → 17 taken 11596 times.
✓ Branch 16 → 18 taken 6 times.
✓ Branch 18 → 19 taken 11602 times.
✗ Branch 18 → 20 not taken.
✓ Branch 20 → 21 taken 11602 times.
✗ Branch 20 → 22 not taken.
✓ Branch 22 → 23 taken 8 times.
✓ Branch 22 → 28 taken 11594 times.
34808 if (val == QStringLiteral("heute") || val == QStringLiteral("today")) return QStringLiteral("today");
60
12/14
✓ Branch 32 → 33 taken 11592 times.
✓ Branch 32 → 38 taken 2 times.
✓ Branch 37 → 38 taken 2 times.
✓ Branch 37 → 39 taken 11590 times.
✓ Branch 40 → 41 taken 11592 times.
✓ Branch 40 → 42 taken 2 times.
✓ Branch 42 → 43 taken 11592 times.
✓ Branch 42 → 44 taken 2 times.
✓ Branch 44 → 45 taken 11594 times.
✗ Branch 44 → 46 not taken.
✓ Branch 46 → 47 taken 11594 times.
✗ Branch 46 → 48 not taken.
✓ Branch 48 → 49 taken 4 times.
✓ Branch 48 → 54 taken 11590 times.
34784 if (val == QStringLiteral("gestern") || val == QStringLiteral("yesterday")) return QStringLiteral("yesterday");
61
12/14
✓ Branch 58 → 59 taken 11588 times.
✓ Branch 58 → 64 taken 2 times.
✓ Branch 63 → 64 taken 4 times.
✓ Branch 63 → 65 taken 11584 times.
✓ Branch 66 → 67 taken 11588 times.
✓ Branch 66 → 68 taken 2 times.
✓ Branch 68 → 69 taken 11588 times.
✓ Branch 68 → 70 taken 2 times.
✓ Branch 70 → 71 taken 11590 times.
✗ Branch 70 → 72 not taken.
✓ Branch 72 → 73 taken 11590 times.
✗ Branch 72 → 74 not taken.
✓ Branch 74 → 75 taken 6 times.
✓ Branch 74 → 80 taken 11584 times.
34774 if (val == QStringLiteral("woche") || val == QStringLiteral("week")) return QStringLiteral("week");
62
12/14
✓ Branch 84 → 85 taken 11580 times.
✓ Branch 84 → 90 taken 4 times.
✓ Branch 89 → 90 taken 20 times.
✓ Branch 89 → 91 taken 11560 times.
✓ Branch 92 → 93 taken 11580 times.
✓ Branch 92 → 94 taken 4 times.
✓ Branch 94 → 95 taken 11580 times.
✓ Branch 94 → 96 taken 4 times.
✓ Branch 96 → 97 taken 11584 times.
✗ Branch 96 → 98 not taken.
✓ Branch 98 → 99 taken 11584 times.
✗ Branch 98 → 100 not taken.
✓ Branch 100 → 101 taken 24 times.
✓ Branch 100 → 106 taken 11560 times.
34772 if (val == QStringLiteral("monat") || val == QStringLiteral("month")) return QStringLiteral("month");
63
12/14
✓ Branch 110 → 111 taken 11558 times.
✓ Branch 110 → 116 taken 2 times.
✓ Branch 115 → 116 taken 2 times.
✓ Branch 115 → 117 taken 11556 times.
✓ Branch 118 → 119 taken 11558 times.
✓ Branch 118 → 120 taken 2 times.
✓ Branch 120 → 121 taken 11558 times.
✓ Branch 120 → 122 taken 2 times.
✓ Branch 122 → 123 taken 11560 times.
✗ Branch 122 → 124 not taken.
✓ Branch 124 → 125 taken 11560 times.
✗ Branch 124 → 126 not taken.
✓ Branch 126 → 127 taken 4 times.
✓ Branch 126 → 132 taken 11556 times.
34682 if (val == QStringLiteral("jahr") || val == QStringLiteral("year")) return QStringLiteral("year");
64 11556 return {};
65 }
66
67 // F-56 (SPRINT-104): ISO first, then the system locale's short date form
68 // (01.03.2025 on a German system) — a local-format date no longer fails
69 // to parse.
70 7012 QDate parseFlexibleDate(const QString &value) {
71
1/1
✓ Branch 2 → 3 taken 7012 times.
7012 QDate d = QDate::fromString(value, Qt::ISODate);
72
3/3
✓ Branch 3 → 4 taken 7012 times.
✓ Branch 4 → 5 taken 1015 times.
✓ Branch 4 → 9 taken 5997 times.
7012 if (!d.isValid())
73
2/2
✓ Branch 5 → 6 taken 1015 times.
✓ Branch 6 → 7 taken 1015 times.
1015 d = QLocale().toDate(value, QLocale::ShortFormat);
74 // Numeric forms the locale may not cover: the German short format is
75 // dd.MM.yy, so the commonly typed 01.03.2025 needs its own pattern; the
76 // dotted, dashed and slashed variants cover the European and US orders.
77
3/3
✓ Branch 9 → 10 taken 7012 times.
✓ Branch 10 → 11 taken 1015 times.
✓ Branch 10 → 18 taken 5997 times.
7012 if (!d.isValid())
78
1/1
✓ Branch 14 → 15 taken 1015 times.
1015 d = QDate::fromString(value, QStringLiteral("d.M.yyyy"));
79
3/3
✓ Branch 18 → 19 taken 7012 times.
✓ Branch 19 → 20 taken 1012 times.
✓ Branch 19 → 27 taken 6000 times.
7012 if (!d.isValid())
80
1/1
✓ Branch 23 → 24 taken 1012 times.
1012 d = QDate::fromString(value, QStringLiteral("d-M-yyyy"));
81
3/3
✓ Branch 27 → 28 taken 7012 times.
✓ Branch 28 → 29 taken 1012 times.
✓ Branch 28 → 36 taken 6000 times.
7012 if (!d.isValid())
82
1/1
✓ Branch 32 → 33 taken 1012 times.
1012 d = QDate::fromString(value, QStringLiteral("d/M/yyyy"));
83
3/3
✓ Branch 36 → 37 taken 7012 times.
✓ Branch 37 → 38 taken 1012 times.
✓ Branch 37 → 45 taken 6000 times.
7012 if (!d.isValid())
84
1/1
✓ Branch 41 → 42 taken 1012 times.
1012 d = QDate::fromString(value, QStringLiteral("M/d/yyyy"));
85
3/3
✓ Branch 45 → 46 taken 7012 times.
✓ Branch 46 → 47 taken 1012 times.
✓ Branch 46 → 54 taken 6000 times.
7012 if (!d.isValid())
86
1/1
✓ Branch 50 → 51 taken 1012 times.
1012 d = QDate::fromString(value, QStringLiteral("d.M.yy"));
87 7012 return d;
88 }
89
90 // A "date:" year ("2025"): four digits in (1900, 2100), else 0. Shared by
91 // single values and range ends, so "-210" or "+999" never becomes a year
92 // whose canonical form cannot be read back.
93 309 int searchYear(const QString &value) {
94
2/2
✓ Branch 3 → 4 taken 11 times.
✓ Branch 3 → 5 taken 298 times.
309 if (value.length() != 4)
95 11 return 0;
96
2/2
✓ Branch 22 → 7 taken 1180 times.
✓ Branch 22 → 23 taken 294 times.
1474 for (const QChar ch : value)
97
6/6
✓ Branch 11 → 12 taken 1178 times.
✓ Branch 11 → 17 taken 2 times.
✓ Branch 16 → 17 taken 2 times.
✓ Branch 16 → 18 taken 1176 times.
✓ Branch 19 → 20 taken 4 times.
✓ Branch 19 → 21 taken 1176 times.
1180 if (ch < QLatin1Char('0') || ch > QLatin1Char('9'))
98 4 return 0;
99 294 const int year = value.toInt();
100
4/4
✓ Branch 24 → 25 taken 291 times.
✓ Branch 24 → 27 taken 3 times.
✓ Branch 25 → 26 taken 290 times.
✓ Branch 25 → 27 taken 1 time.
294 return year > 1900 && year < 2100 ? year : 0;
101 }
102
103 // Parse a "date:" value into an inclusive [from, to] range. Supports presets
104 // (heute/gestern/woche/monat/jahr and today/yesterday/week/month/year), ISO
105 // and local short dates (year/month/day) and "A..B"
106 // ranges with open ends.
107 6232 QPair<QDateTime, QDateTime> parseDateValue(const QString &val) {
108 6232 QDateTime from, to;
109
1/1
✓ Branch 4 → 5 taken 6232 times.
6232 const QDate today = QDate::currentDate();
110
111
1/1
✓ Branch 8 → 9 taken 6232 times.
6232 const int dotdot = val.indexOf(QStringLiteral(".."));
112
2/2
✓ Branch 11 → 12 taken 834 times.
✓ Branch 11 → 84 taken 5398 times.
6232 if (dotdot >= 0) {
113
1/1
✓ Branch 12 → 13 taken 834 times.
834 const QString a = val.left(dotdot);
114
1/1
✓ Branch 13 → 14 taken 834 times.
834 const QString b = val.mid(dotdot + 2);
115
2/2
✓ Branch 15 → 16 taken 820 times.
✓ Branch 15 → 46 taken 14 times.
834 if (!a.isEmpty()) {
116
1/1
✓ Branch 16 → 17 taken 820 times.
820 QDate d = parseFlexibleDate(a);
117
7/7
✓ Branch 17 → 18 taken 820 times.
✓ Branch 18 → 19 taken 26 times.
✓ Branch 18 → 22 taken 794 times.
✓ Branch 20 → 21 taken 11 times.
✓ Branch 20 → 22 taken 15 times.
✓ Branch 23 → 24 taken 11 times.
✓ Branch 23 → 28 taken 809 times.
820 if (!d.isValid() && a.length() == 7) // "2025-02"
118
2/2
✓ Branch 24 → 25 taken 11 times.
✓ Branch 25 → 26 taken 11 times.
11 d = QDate::fromString(a + "-01", Qt::ISODate);
119
8/8
✓ Branch 28 → 29 taken 820 times.
✓ Branch 29 → 30 taken 16 times.
✓ Branch 29 → 33 taken 804 times.
✓ Branch 30 → 31 taken 16 times.
✓ Branch 31 → 32 taken 12 times.
✓ Branch 31 → 33 taken 4 times.
✓ Branch 34 → 35 taken 12 times.
✓ Branch 34 → 38 taken 808 times.
820 if (!d.isValid() && searchYear(a) > 0) // "2025"
120
2/2
✓ Branch 35 → 36 taken 12 times.
✓ Branch 36 → 37 taken 12 times.
12 d = QDate(searchYear(a), 1, 1);
121
3/3
✓ Branch 38 → 39 taken 820 times.
✓ Branch 39 → 40 taken 816 times.
✓ Branch 39 → 45 taken 4 times.
820 if (d.isValid())
122
2/2
✓ Branch 40 → 41 taken 816 times.
✓ Branch 41 → 42 taken 816 times.
816 from = QDateTime(d, QTime(0, 0));
123 }
124
2/2
✓ Branch 47 → 48 taken 817 times.
✓ Branch 47 → 80 taken 17 times.
834 if (!b.isEmpty()) {
125
1/1
✓ Branch 48 → 49 taken 817 times.
817 QDate d = parseFlexibleDate(b);
126
7/7
✓ Branch 49 → 50 taken 817 times.
✓ Branch 50 → 51 taken 18 times.
✓ Branch 50 → 54 taken 799 times.
✓ Branch 52 → 53 taken 12 times.
✓ Branch 52 → 54 taken 6 times.
✓ Branch 55 → 56 taken 12 times.
✓ Branch 55 → 62 taken 805 times.
817 if (!d.isValid() && b.length() == 7)
127
4/4
✓ Branch 56 → 57 taken 12 times.
✓ Branch 57 → 58 taken 12 times.
✓ Branch 58 → 59 taken 12 times.
✓ Branch 59 → 60 taken 12 times.
12 d = QDate::fromString(b + "-01", Qt::ISODate).addMonths(1).addDays(-1);
128
8/8
✓ Branch 62 → 63 taken 817 times.
✓ Branch 63 → 64 taken 7 times.
✓ Branch 63 → 67 taken 810 times.
✓ Branch 64 → 65 taken 7 times.
✓ Branch 65 → 66 taken 1 time.
✓ Branch 65 → 67 taken 6 times.
✓ Branch 68 → 69 taken 1 time.
✓ Branch 68 → 72 taken 816 times.
817 if (!d.isValid() && searchYear(b) > 0)
129
2/2
✓ Branch 69 → 70 taken 1 time.
✓ Branch 70 → 71 taken 1 time.
1 d = QDate(searchYear(b), 12, 31);
130
3/3
✓ Branch 72 → 73 taken 817 times.
✓ Branch 73 → 74 taken 811 times.
✓ Branch 73 → 79 taken 6 times.
817 if (d.isValid())
131
2/2
✓ Branch 74 → 75 taken 811 times.
✓ Branch 75 → 76 taken 811 times.
811 to = QDateTime(d, QTime(23, 59, 59));
132 }
133 834 return {from, to};
134 834 }
135
136 // Presets in German and English (Sprint 98: SearchVocabulary.h lists the
137 // English words; the German ones are its keywords and stay accepted).
138
1/1
✓ Branch 84 → 85 taken 5398 times.
5398 const QString preset = canonicalDatePreset(val);
139
2/2
✓ Branch 91 → 92 taken 4 times.
✓ Branch 91 → 101 taken 5394 times.
5398 if (preset == QStringLiteral("today")) {
140
2/2
✓ Branch 92 → 93 taken 4 times.
✓ Branch 93 → 94 taken 4 times.
4 from = QDateTime(today, QTime(0, 0));
141
2/2
✓ Branch 96 → 97 taken 4 times.
✓ Branch 97 → 98 taken 4 times.
4 to = QDateTime(today, QTime(23, 59, 59));
142
2/2
✓ Branch 107 → 108 taken 2 times.
✓ Branch 107 → 118 taken 5392 times.
5394 } else if (preset == QStringLiteral("yesterday")) {
143
1/1
✓ Branch 108 → 109 taken 2 times.
2 const QDate y = today.addDays(-1);
144
2/2
✓ Branch 109 → 110 taken 2 times.
✓ Branch 110 → 111 taken 2 times.
2 from = QDateTime(y, QTime(0, 0));
145
2/2
✓ Branch 113 → 114 taken 2 times.
✓ Branch 114 → 115 taken 2 times.
2 to = QDateTime(y, QTime(23, 59, 59));
146
2/2
✓ Branch 124 → 125 taken 3 times.
✓ Branch 124 → 135 taken 5389 times.
5392 } else if (preset == QStringLiteral("week")) {
147
3/3
✓ Branch 125 → 126 taken 3 times.
✓ Branch 126 → 127 taken 3 times.
✓ Branch 127 → 128 taken 3 times.
3 from = QDateTime(today.addDays(-7), QTime(0, 0));
148
2/2
✓ Branch 130 → 131 taken 3 times.
✓ Branch 131 → 132 taken 3 times.
3 to = QDateTime(today, QTime(23, 59, 59));
149
2/2
✓ Branch 141 → 142 taken 12 times.
✓ Branch 141 → 152 taken 5377 times.
5389 } else if (preset == QStringLiteral("month")) {
150
3/3
✓ Branch 142 → 143 taken 12 times.
✓ Branch 143 → 144 taken 12 times.
✓ Branch 144 → 145 taken 12 times.
12 from = QDateTime(today.addDays(-30), QTime(0, 0));
151
2/2
✓ Branch 147 → 148 taken 12 times.
✓ Branch 148 → 149 taken 12 times.
12 to = QDateTime(today, QTime(23, 59, 59));
152
2/2
✓ Branch 158 → 159 taken 2 times.
✓ Branch 158 → 169 taken 5375 times.
5377 } else if (preset == QStringLiteral("year")) {
153
3/3
✓ Branch 159 → 160 taken 2 times.
✓ Branch 160 → 161 taken 2 times.
✓ Branch 161 → 162 taken 2 times.
2 from = QDateTime(today.addYears(-1), QTime(0, 0));
154
2/2
✓ Branch 164 → 165 taken 2 times.
✓ Branch 165 → 166 taken 2 times.
2 to = QDateTime(today, QTime(23, 59, 59));
155 } else {
156
1/1
✓ Branch 169 → 170 taken 5375 times.
5375 QDate d = parseFlexibleDate(val);
157
3/3
✓ Branch 170 → 171 taken 5375 times.
✓ Branch 171 → 172 taken 4407 times.
✓ Branch 171 → 181 taken 968 times.
5375 if (d.isValid()) {
158
2/2
✓ Branch 172 → 173 taken 4407 times.
✓ Branch 173 → 174 taken 4407 times.
4407 from = QDateTime(d, QTime(0, 0));
159
2/2
✓ Branch 176 → 177 taken 4407 times.
✓ Branch 177 → 178 taken 4407 times.
4407 to = QDateTime(d, QTime(23, 59, 59));
160
2/2
✓ Branch 182 → 183 taken 695 times.
✓ Branch 182 → 200 taken 273 times.
968 } else if (val.length() == 7) { // "2025-02"
161
2/2
✓ Branch 183 → 184 taken 695 times.
✓ Branch 184 → 185 taken 695 times.
695 const QDate start = QDate::fromString(val + "-01", Qt::ISODate);
162
3/3
✓ Branch 186 → 187 taken 695 times.
✓ Branch 187 → 188 taken 694 times.
✓ Branch 187 → 199 taken 1 time.
695 if (start.isValid()) {
163
2/2
✓ Branch 188 → 189 taken 694 times.
✓ Branch 189 → 190 taken 694 times.
694 from = QDateTime(start, QTime(0, 0));
164
4/4
✓ Branch 192 → 193 taken 694 times.
✓ Branch 193 → 194 taken 694 times.
✓ Branch 194 → 195 taken 694 times.
✓ Branch 195 → 196 taken 694 times.
694 to = QDateTime(start.addMonths(1).addDays(-1), QTime(23, 59, 59));
165 }
166
3/3
✓ Branch 200 → 201 taken 273 times.
✓ Branch 201 → 202 taken 264 times.
✓ Branch 201 → 213 taken 9 times.
273 } else if (const int year = searchYear(val); year > 0) { // "2025"
167
3/3
✓ Branch 202 → 203 taken 264 times.
✓ Branch 203 → 204 taken 264 times.
✓ Branch 204 → 205 taken 264 times.
264 from = QDateTime(QDate(year, 1, 1), QTime(0, 0));
168
3/3
✓ Branch 207 → 208 taken 264 times.
✓ Branch 208 → 209 taken 264 times.
✓ Branch 209 → 210 taken 264 times.
264 to = QDateTime(QDate(year, 12, 31), QTime(23, 59, 59));
169 }
170 }
171 5398 return {from, to};
172 6232 }
173
174 // Quote a value for serialization if it contains whitespace (or is empty), so
175 // it survives tokenization on the way back in.
176 325 QString quoteIfNeeded(const QString &value) {
177 325 bool needsQuote = value.isEmpty();
178
2/2
✓ Branch 16 → 5 taken 2493 times.
✓ Branch 16 → 17 taken 308 times.
2801 for (const QChar ch : value) {
179
5/6
✓ Branch 6 → 7 taken 2476 times.
✓ Branch 6 → 11 taken 17 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 2476 times.
✓ Branch 13 → 14 taken 17 times.
✓ Branch 13 → 15 taken 2476 times.
2493 if (ch.isSpace() || ch == QLatin1Char('"')) {
180 17 needsQuote = true;
181 17 break;
182 }
183 }
184
2/2
✓ Branch 17 → 18 taken 306 times.
✓ Branch 17 → 19 taken 19 times.
325 if (!needsQuote)
185 306 return value;
186 19 QString escaped = value;
187
1/1
✓ Branch 25 → 26 taken 19 times.
19 escaped.replace(QLatin1Char('\\'), QStringLiteral("\\\\"));
188
1/1
✓ Branch 33 → 34 taken 19 times.
19 escaped.replace(QLatin1Char('"'), QStringLiteral("\\\""));
189
2/2
✓ Branch 40 → 41 taken 19 times.
✓ Branch 41 → 42 taken 19 times.
38 return QLatin1Char('"') + escaped + QLatin1Char('"');
190 19 }
191
192 110958 bool startsWithCi(const QString &s, const QString &prefix) {
193 110958 return s.startsWith(prefix, Qt::CaseInsensitive);
194 }
195
196 } // namespace
197
198
199 namespace {
200
201 // is:/has: values. Returns false for an unknown value (kept as free text).
202 802 bool applyState(const QString &prefix, const QString &value, bool negated,
203 MailCache::SearchFilter &filter) {
204
2/2
✓ Branch 2 → 3 taken 706 times.
✓ Branch 2 → 4 taken 89 times.
1597 const auto set = [negated](Tri &field, bool yes) { field = (yes != negated) ? Tri::Yes : Tri::No; };
205
2/2
✓ Branch 8 → 9 taken 586 times.
✓ Branch 8 → 87 taken 216 times.
802 if (prefix == QStringLiteral("is:")) {
206
2/2
✓ Branch 15 → 16 taken 430 times.
✓ Branch 15 → 17 taken 156 times.
586 if (value == QStringLiteral("unread")) set(filter.unread, true);
207
2/2
✓ Branch 23 → 24 taken 19 times.
✓ Branch 23 → 25 taken 137 times.
156 else if (value == QStringLiteral("read")) set(filter.unread, false);
208
12/14
✓ Branch 29 → 30 taken 134 times.
✓ Branch 29 → 35 taken 3 times.
✓ Branch 34 → 35 taken 65 times.
✓ Branch 34 → 36 taken 69 times.
✓ Branch 37 → 38 taken 134 times.
✓ Branch 37 → 39 taken 3 times.
✓ Branch 39 → 40 taken 134 times.
✓ Branch 39 → 41 taken 3 times.
✓ Branch 41 → 42 taken 137 times.
✗ Branch 41 → 43 not taken.
✓ Branch 43 → 44 taken 137 times.
✗ Branch 43 → 45 not taken.
✓ Branch 45 → 46 taken 68 times.
✓ Branch 45 → 47 taken 69 times.
408 else if (value == QStringLiteral("starred") || value == QStringLiteral("flagged")) set(filter.flagged, true);
209
12/14
✓ Branch 51 → 52 taken 57 times.
✓ Branch 51 → 57 taken 12 times.
✓ Branch 56 → 57 taken 13 times.
✓ Branch 56 → 58 taken 44 times.
✓ Branch 59 → 60 taken 57 times.
✓ Branch 59 → 61 taken 12 times.
✓ Branch 61 → 62 taken 57 times.
✓ Branch 61 → 63 taken 12 times.
✓ Branch 63 → 64 taken 69 times.
✗ Branch 63 → 65 not taken.
✓ Branch 65 → 66 taken 69 times.
✗ Branch 65 → 67 not taken.
✓ Branch 67 → 68 taken 25 times.
✓ Branch 67 → 69 taken 44 times.
195 else if (value == QStringLiteral("unstarred") || value == QStringLiteral("unflagged")) set(filter.flagged, false);
210
2/2
✓ Branch 75 → 76 taken 25 times.
✓ Branch 75 → 77 taken 19 times.
44 else if (value == QStringLiteral("answered")) set(filter.answered, true);
211
2/2
✓ Branch 83 → 84 taken 15 times.
✓ Branch 83 → 85 taken 4 times.
19 else if (value == QStringLiteral("unanswered")) set(filter.answered, false);
212 4 else return false;
213 582 return true;
214 }
215
12/14
✓ Branch 91 → 92 taken 24 times.
✓ Branch 91 → 97 taken 192 times.
✓ Branch 96 → 97 taken 2 times.
✓ Branch 96 → 98 taken 22 times.
✓ Branch 99 → 100 taken 24 times.
✓ Branch 99 → 101 taken 192 times.
✓ Branch 101 → 102 taken 24 times.
✓ Branch 101 → 103 taken 192 times.
✓ Branch 103 → 104 taken 216 times.
✗ Branch 103 → 105 not taken.
✓ Branch 105 → 106 taken 216 times.
✗ Branch 105 → 107 not taken.
✓ Branch 107 → 108 taken 194 times.
✓ Branch 107 → 109 taken 22 times.
456 if (value == QStringLiteral("attachment") || value == QStringLiteral("attachments")) set(filter.hasAttachment, true);
216
12/14
✓ Branch 113 → 114 taken 5 times.
✓ Branch 113 → 119 taken 17 times.
✓ Branch 118 → 119 taken 2 times.
✓ Branch 118 → 120 taken 3 times.
✓ Branch 121 → 122 taken 5 times.
✓ Branch 121 → 123 taken 17 times.
✓ Branch 123 → 124 taken 5 times.
✓ Branch 123 → 125 taken 17 times.
✓ Branch 125 → 126 taken 22 times.
✗ Branch 125 → 127 not taken.
✓ Branch 127 → 128 taken 22 times.
✗ Branch 127 → 129 not taken.
✓ Branch 129 → 130 taken 19 times.
✓ Branch 129 → 131 taken 3 times.
49 else if (value == QStringLiteral("noattachment") || value == QStringLiteral("noattachments")) set(filter.hasAttachment, false);
217 3 else return false;
218 213 return true;
219 }
220
221 // A "prefix:value" token that constrains the search. Returns false when the
222 // token names no known prefix (the caller keeps it as free text).
223 18102 bool applyPositive(const QString &token, MailCache::SearchFilter &filter) {
224 28695 const auto valueAfter = [&token](int length) { return token.mid(length); };
225
3/3
✓ Branch 5 → 6 taken 18102 times.
✓ Branch 8 → 9 taken 339 times.
✓ Branch 8 → 17 taken 17763 times.
18102 if (startsWithCi(token, QStringLiteral("folder:"))) {
226 // Sprint 60 (Q1): repeatable — every folder: token (OR-semantics).
227
5/5
✓ Branch 9 → 10 taken 339 times.
✓ Branch 12 → 13 taken 338 times.
✓ Branch 12 → 177 taken 1 time.
✓ Branch 13 → 14 taken 338 times.
✓ Branch 14 → 15 taken 338 times.
339 if (!valueAfter(7).isEmpty()) filter.folderPatterns.append(valueAfter(7));
228
3/3
✓ Branch 20 → 21 taken 17763 times.
✓ Branch 23 → 24 taken 6232 times.
✓ Branch 23 → 56 taken 11531 times.
17763 } else if (startsWithCi(token, QStringLiteral("date:"))) {
229
2/2
✓ Branch 24 → 25 taken 6232 times.
✓ Branch 25 → 26 taken 6232 times.
6232 const QString value = valueAfter(5).toLower();
230
1/1
✓ Branch 27 → 28 taken 6232 times.
6232 const auto [from, to] = parseDateValue(value);
231
8/8
✓ Branch 30 → 31 taken 6232 times.
✓ Branch 31 → 32 taken 28 times.
✓ Branch 31 → 35 taken 6204 times.
✓ Branch 32 → 33 taken 28 times.
✓ Branch 33 → 34 taken 15 times.
✓ Branch 33 → 35 taken 13 times.
✓ Branch 36 → 37 taken 15 times.
✓ Branch 36 → 38 taken 6217 times.
6232 if (!from.isValid() && !to.isValid()) {
232 // F-56 (SPRINT-104): an unreadable value must not silently widen the
233 // search — it stays in the query as visible free text (like unknown
234 // prefixes), so the user sees the limit is not applied.
235 15 return false;
236 }
237 6217 filter.dateFrom = from;
238 6217 filter.dateTo = to;
239
4/4
✓ Branch 40 → 41 taken 6217 times.
✓ Branch 41 → 42 taken 6204 times.
✓ Branch 41 → 43 taken 13 times.
✓ Branch 42 → 44 taken 6204 times.
6217 filter.datePreset = from.isValid() ? canonicalDatePreset(value) : QString();
240
7/7
✓ Branch 48 → 49 taken 6217 times.
✓ Branch 48 → 50 taken 15 times.
✓ Branch 52 → 53 taken 6217 times.
✓ Branch 52 → 55 taken 15 times.
✓ Branch 59 → 60 taken 11531 times.
✓ Branch 62 → 63 taken 3327 times.
✓ Branch 62 → 67 taken 8204 times.
17778 } else if (startsWithCi(token, QStringLiteral("from:"))) {
241
1/1
✓ Branch 63 → 64 taken 3327 times.
3327 filter.fromFilter = valueAfter(5);
242
3/3
✓ Branch 70 → 71 taken 8204 times.
✓ Branch 73 → 74 taken 16 times.
✓ Branch 73 → 78 taken 8188 times.
8204 } else if (startsWithCi(token, QStringLiteral("to:"))) {
243
1/1
✓ Branch 74 → 75 taken 16 times.
16 filter.toFilter = valueAfter(3);
244
3/3
✓ Branch 81 → 82 taken 8188 times.
✓ Branch 84 → 85 taken 50 times.
✓ Branch 84 → 89 taken 8138 times.
8188 } else if (startsWithCi(token, QStringLiteral("list:"))) {
245
1/1
✓ Branch 85 → 86 taken 50 times.
50 filter.listFilter = valueAfter(5); // Sprint 95: List-Id contains
246
3/3
✓ Branch 92 → 93 taken 8138 times.
✓ Branch 95 → 96 taken 7 times.
✓ Branch 95 → 100 taken 8131 times.
8138 } else if (startsWithCi(token, QStringLiteral("about:"))) {
247
1/1
✓ Branch 96 → 97 taken 7 times.
7 filter.aboutFilter = valueAfter(6); // Sprint 103 D-21: mails linked to a contact
248
3/3
✓ Branch 103 → 104 taken 8131 times.
✓ Branch 106 → 107 taken 190 times.
✓ Branch 106 → 111 taken 7941 times.
8131 } else if (startsWithCi(token, QStringLiteral("subject:"))) {
249
1/1
✓ Branch 107 → 108 taken 190 times.
190 filter.subjectFilter = valueAfter(8);
250
14/24
✓ Branch 114 → 115 taken 7941 times.
✓ Branch 115 → 116 taken 7851 times.
✓ Branch 115 → 121 taken 90 times.
✓ Branch 119 → 120 taken 7851 times.
✓ Branch 120 → 121 taken 4 times.
✓ Branch 120 → 122 taken 7847 times.
✓ Branch 123 → 124 taken 7851 times.
✓ Branch 123 → 125 taken 90 times.
✓ Branch 125 → 126 taken 7851 times.
✓ Branch 125 → 127 taken 90 times.
✓ Branch 127 → 128 taken 7941 times.
✗ Branch 127 → 129 not taken.
✓ Branch 129 → 130 taken 7941 times.
✗ Branch 129 → 131 not taken.
✓ Branch 131 → 132 taken 94 times.
✓ Branch 131 → 141 taken 7847 times.
✗ Branch 240 → 241 not taken.
✗ Branch 240 → 242 not taken.
✗ Branch 244 → 245 not taken.
✗ Branch 244 → 246 not taken.
✗ Branch 248 → 249 not taken.
✗ Branch 248 → 250 not taken.
✗ Branch 252 → 253 not taken.
✗ Branch 252 → 254 not taken.
23733 } else if (startsWithCi(token, QStringLiteral("tag:")) || startsWithCi(token, QStringLiteral("label:"))) {
251
1/1
✓ Branch 135 → 136 taken 94 times.
94 const QString value = valueAfter(token.indexOf(QLatin1Char(':')) + 1);
252
3/3
✓ Branch 137 → 138 taken 89 times.
✓ Branch 137 → 139 taken 5 times.
✓ Branch 138 → 139 taken 89 times.
94 if (!value.isEmpty()) filter.tags.append(value);
253
14/24
✓ Branch 144 → 145 taken 7847 times.
✓ Branch 145 → 146 taken 7262 times.
✓ Branch 145 → 151 taken 585 times.
✓ Branch 149 → 150 taken 7262 times.
✓ Branch 150 → 151 taken 206 times.
✓ Branch 150 → 152 taken 7056 times.
✓ Branch 153 → 154 taken 7262 times.
✓ Branch 153 → 155 taken 585 times.
✓ Branch 155 → 156 taken 7262 times.
✓ Branch 155 → 157 taken 585 times.
✓ Branch 157 → 158 taken 7847 times.
✗ Branch 157 → 159 not taken.
✓ Branch 159 → 160 taken 7847 times.
✗ Branch 159 → 161 not taken.
✓ Branch 161 → 162 taken 791 times.
✓ Branch 161 → 176 taken 7056 times.
✗ Branch 261 → 262 not taken.
✗ Branch 261 → 263 not taken.
✗ Branch 265 → 266 not taken.
✗ Branch 265 → 267 not taken.
✗ Branch 269 → 270 not taken.
✗ Branch 269 → 271 not taken.
✗ Branch 273 → 274 not taken.
✗ Branch 273 → 275 not taken.
23050 } else if (startsWithCi(token, QStringLiteral("is:")) || startsWithCi(token, QStringLiteral("has:"))) {
254 791 const int colon = token.indexOf(QLatin1Char(':'));
255
5/5
✓ Branch 165 → 166 taken 791 times.
✓ Branch 166 → 167 taken 791 times.
✓ Branch 167 → 168 taken 791 times.
✓ Branch 168 → 169 taken 791 times.
✓ Branch 169 → 170 taken 791 times.
791 return applyState(token.left(colon + 1).toLower(), token.mid(colon + 1).toLower(), false, filter);
256 } else {
257 7056 return false;
258 }
259 10240 return true;
260 }
261
262 // S101.1.8: "-prefix:value" and "-word". Returns false when the token is no
263 // negation (the caller keeps it as free text).
264 7078 bool applyNegated(const QString &token, MailCache::SearchFilter &filter) {
265
8/8
✓ Branch 3 → 4 taken 7033 times.
✓ Branch 3 → 12 taken 45 times.
✓ Branch 8 → 9 taken 375 times.
✓ Branch 8 → 12 taken 6658 times.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 374 times.
✓ Branch 14 → 15 taken 6704 times.
✓ Branch 14 → 16 taken 374 times.
7078 if (token.size() < 2 || token.at(0) != QLatin1Char('-') || !token.at(1).isLetterOrNumber()) return false;
266
1/1
✓ Branch 16 → 17 taken 374 times.
374 const QString rest = token.mid(1);
267 374 const int colon = rest.indexOf(QLatin1Char(':'));
268
6/8
✓ Branch 20 → 21 taken 330 times.
✓ Branch 20 → 23 taken 44 times.
✓ Branch 21 → 22 taken 330 times.
✓ Branch 22 → 24 taken 330 times.
✓ Branch 24 → 25 taken 330 times.
✓ Branch 24 → 26 taken 44 times.
✗ Branch 153 → 154 not taken.
✗ Branch 153 → 155 not taken.
374 const QString prefix = colon > 0 ? rest.left(colon + 1).toLower() : QString();
269
3/3
✓ Branch 26 → 27 taken 330 times.
✓ Branch 26 → 28 taken 44 times.
✓ Branch 27 → 29 taken 330 times.
374 const QString value = colon > 0 ? rest.mid(colon + 1) : rest;
270
4/6
✓ Branch 29 → 30 taken 330 times.
✓ Branch 29 → 33 taken 44 times.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 330 times.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 374 times.
374 if (colon > 0 && value.isEmpty()) return false;
271
3/3
✓ Branch 42 → 43 taken 66 times.
✓ Branch 42 → 44 taken 308 times.
✓ Branch 43 → 147 taken 66 times.
374 if (prefix == QStringLiteral("from:")) filter.excludedFrom.append(value);
272
3/3
✓ Branch 50 → 51 taken 5 times.
✓ Branch 50 → 52 taken 303 times.
✓ Branch 51 → 147 taken 5 times.
308 else if (prefix == QStringLiteral("to:")) filter.excludedTo.append(value);
273
3/3
✓ Branch 58 → 59 taken 4 times.
✓ Branch 58 → 60 taken 299 times.
✓ Branch 59 → 147 taken 4 times.
303 else if (prefix == QStringLiteral("subject:")) filter.excludedSubjects.append(value);
274
3/3
✓ Branch 66 → 67 taken 218 times.
✓ Branch 66 → 68 taken 81 times.
✓ Branch 67 → 147 taken 218 times.
299 else if (prefix == QStringLiteral("folder:")) filter.excludedFolders.append(value);
275
3/3
✓ Branch 74 → 75 taken 12 times.
✓ Branch 74 → 76 taken 69 times.
✓ Branch 75 → 147 taken 12 times.
81 else if (prefix == QStringLiteral("list:")) filter.excludedLists.append(value);
276
12/15
✓ Branch 80 → 81 taken 56 times.
✓ Branch 80 → 86 taken 13 times.
✗ Branch 85 → 86 not taken.
✓ Branch 85 → 87 taken 56 times.
✓ Branch 88 → 89 taken 56 times.
✓ Branch 88 → 90 taken 13 times.
✓ Branch 90 → 91 taken 56 times.
✓ Branch 90 → 92 taken 13 times.
✓ Branch 92 → 93 taken 69 times.
✗ Branch 92 → 94 not taken.
✓ Branch 94 → 95 taken 69 times.
✗ Branch 94 → 96 not taken.
✓ Branch 96 → 97 taken 13 times.
✓ Branch 96 → 98 taken 56 times.
✓ Branch 97 → 147 taken 13 times.
194 else if (prefix == QStringLiteral("tag:") || prefix == QStringLiteral("label:")) filter.excludedTags.append(value);
277
14/16
✓ Branch 102 → 103 taken 55 times.
✓ Branch 102 → 108 taken 1 time.
✓ Branch 107 → 108 taken 10 times.
✓ Branch 107 → 109 taken 45 times.
✓ Branch 110 → 111 taken 55 times.
✓ Branch 110 → 112 taken 1 time.
✓ Branch 112 → 113 taken 55 times.
✓ Branch 112 → 114 taken 1 time.
✓ Branch 114 → 115 taken 56 times.
✗ Branch 114 → 116 not taken.
✓ Branch 116 → 117 taken 56 times.
✗ Branch 116 → 118 not taken.
✓ Branch 118 → 119 taken 11 times.
✓ Branch 118 → 124 taken 45 times.
✓ Branch 119 → 120 taken 11 times.
✓ Branch 120 → 121 taken 11 times.
167 else if (prefix == QStringLiteral("is:") || prefix == QStringLiteral("has:")) return applyState(prefix, value.toLower(), true, filter);
278
11/14
✓ Branch 128 → 129 taken 44 times.
✓ Branch 128 → 134 taken 1 time.
✗ Branch 133 → 134 not taken.
✓ Branch 133 → 135 taken 44 times.
✓ Branch 136 → 137 taken 44 times.
✓ Branch 136 → 138 taken 1 time.
✓ Branch 138 → 139 taken 44 times.
✓ Branch 138 → 140 taken 1 time.
✓ Branch 140 → 141 taken 45 times.
✗ Branch 140 → 142 not taken.
✓ Branch 142 → 143 taken 45 times.
✗ Branch 142 → 144 not taken.
✓ Branch 144 → 145 taken 1 time.
✓ Branch 144 → 146 taken 44 times.
134 else if (prefix == QStringLiteral("date:") || prefix == QStringLiteral("about:")) return false; // kept as free text
279
1/1
✓ Branch 146 → 147 taken 44 times.
44 else filter.excludedTerms.append(rest);
280 362 return true;
281 374 }
282
283 5180 void appendTri(QStringList &parts, Tri value, const char *yes, const char *no) {
284
4/4
✓ Branch 2 → 3 taken 86 times.
✓ Branch 2 → 8 taken 5094 times.
✓ Branch 4 → 5 taken 86 times.
✓ Branch 5 → 6 taken 86 times.
5180 if (value == Tri::Yes) parts.append(QString::fromLatin1(yes));
285
4/4
✓ Branch 8 → 9 taken 14 times.
✓ Branch 8 → 14 taken 5080 times.
✓ Branch 10 → 11 taken 14 times.
✓ Branch 11 → 12 taken 14 times.
5094 else if (value == Tri::No) parts.append(QString::fromLatin1(no));
286 5180 }
287
288 10360 void appendEach(QStringList &parts, const char *prefix, const QStringList &values, bool keepEmpty = false) {
289
2/2
✓ Branch 22 → 4 taken 110 times.
✓ Branch 22 → 23 taken 10360 times.
10470 for (const QString &value : values)
290
10/10
✓ Branch 5 → 6 taken 99 times.
✓ Branch 5 → 8 taken 11 times.
✓ Branch 7 → 8 taken 97 times.
✓ Branch 7 → 9 taken 2 times.
✓ Branch 10 → 11 taken 108 times.
✓ Branch 10 → 20 taken 2 times.
✓ Branch 11 → 12 taken 108 times.
✓ Branch 13 → 14 taken 108 times.
✓ Branch 14 → 15 taken 108 times.
✓ Branch 15 → 16 taken 108 times.
110 if (keepEmpty || !value.isEmpty()) parts.append(QString::fromLatin1(prefix) + quoteIfNeeded(value));
291 10360 }
292
293 } // namespace
294
295 5570 QString canonicalDateToken(const QDate &from, const QDate &to) {
296
4/6
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 7 taken 5567 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 3 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 5570 times.
5570 if (!from.isValid() && !to.isValid()) return {};
297
6/6
✓ Branch 11 → 12 taken 5567 times.
✓ Branch 11 → 15 taken 3 times.
✓ Branch 13 → 14 taken 5559 times.
✓ Branch 13 → 15 taken 8 times.
✓ Branch 16 → 17 taken 5559 times.
✓ Branch 16 → 50 taken 11 times.
5570 if (from.isValid() && to.isValid()) {
298
2/2
✓ Branch 18 → 19 taken 4387 times.
✓ Branch 18 → 20 taken 1172 times.
5559 if (from == to) return from.toString(Qt::ISODate);
299
9/9
✓ Branch 20 → 21 taken 1172 times.
✓ Branch 21 → 22 taken 549 times.
✓ Branch 21 → 27 taken 623 times.
✓ Branch 22 → 23 taken 549 times.
✓ Branch 23 → 24 taken 549 times.
✓ Branch 25 → 26 taken 351 times.
✓ Branch 25 → 27 taken 198 times.
✓ Branch 28 → 29 taken 351 times.
✓ Branch 28 → 37 taken 821 times.
1172 if (from.day() == 1 && to == from.addMonths(1).addDays(-1))
300
1/1
✓ Branch 32 → 33 taken 351 times.
351 return from.toString(QStringLiteral("yyyy-MM"));
301
12/12
✓ Branch 37 → 38 taken 821 times.
✓ Branch 38 → 39 taken 227 times.
✓ Branch 38 → 46 taken 594 times.
✓ Branch 39 → 40 taken 227 times.
✓ Branch 40 → 41 taken 175 times.
✓ Branch 40 → 46 taken 52 times.
✓ Branch 41 → 42 taken 175 times.
✓ Branch 42 → 43 taken 175 times.
✓ Branch 44 → 45 taken 123 times.
✓ Branch 44 → 46 taken 52 times.
✓ Branch 47 → 48 taken 123 times.
✓ Branch 47 → 50 taken 698 times.
821 if (from.month() == 1 && from.day() == 1 && to == QDate(from.year(), 12, 31))
302 123 return QString::number(from.year());
303 }
304
5/5
✓ Branch 57 → 58 taken 709 times.
✓ Branch 58 → 59 taken 706 times.
✓ Branch 58 → 60 taken 3 times.
✓ Branch 59 → 61 taken 706 times.
✓ Branch 61 → 62 taken 709 times.
1418 return (from.isValid() ? from.toString(Qt::ISODate) : QString()) + QStringLiteral("..") +
305
5/5
✓ Branch 50 → 51 taken 709 times.
✓ Branch 51 → 52 taken 701 times.
✓ Branch 51 → 53 taken 8 times.
✓ Branch 52 → 54 taken 701 times.
✓ Branch 62 → 63 taken 709 times.
2127 (to.isValid() ? to.toString(Qt::ISODate) : QString());
306 }
307
308 13903 QString parseSearchQuery(const QString &input,
309 MailCache::SearchFilter &filter) {
310 13903 QStringList remaining;
311
5/5
✓ Branch 2 → 3 taken 13903 times.
✓ Branch 3 → 4 taken 13903 times.
✓ Branch 4 → 5 taken 13903 times.
✓ Branch 45 → 6 taken 17127 times.
✓ Branch 45 → 46 taken 13903 times.
31030 for (const auto &token : tokenize(input)) {
312 // S106-207: buildSearchQuery writes an empty label as tag:"" — read it
313 // back as one, so parse(build(text, f)) == f holds for it too. A bare
314 // "tag:" (no quotes) still adds nothing.
315
6/6
✓ Branch 7 → 8 taken 17120 times.
✓ Branch 7 → 20 taken 7 times.
✓ Branch 8 → 9 taken 22 times.
✓ Branch 8 → 20 taken 17098 times.
✓ Branch 13 → 14 taken 21 times.
✓ Branch 13 → 19 taken 1 time.
17170 if (!token.literal && token.quoted &&
316
5/6
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 21 times.
✓ Branch 27 → 28 taken 22 times.
✓ Branch 27 → 29 taken 17105 times.
✓ Branch 29 → 30 taken 1 time.
✓ Branch 29 → 34 taken 17126 times.
17192 (token.text.compare(QStringLiteral("tag:"), Qt::CaseInsensitive) == 0 ||
317
6/6
✓ Branch 21 → 22 taken 21 times.
✓ Branch 21 → 23 taken 17106 times.
✓ Branch 23 → 24 taken 21 times.
✓ Branch 23 → 25 taken 17106 times.
✓ Branch 25 → 26 taken 22 times.
✓ Branch 25 → 27 taken 17105 times.
17169 token.text.compare(QStringLiteral("label:"), Qt::CaseInsensitive) == 0)) {
318
1/1
✓ Branch 31 → 32 taken 1 time.
1 filter.tags.append(QString());
319 1 continue;
320 }
321
10/10
✓ Branch 34 → 35 taken 17119 times.
✓ Branch 34 → 39 taken 7 times.
✓ Branch 35 → 36 taken 17119 times.
✓ Branch 36 → 37 taken 6096 times.
✓ Branch 36 → 40 taken 11023 times.
✓ Branch 37 → 38 taken 6096 times.
✓ Branch 38 → 39 taken 5724 times.
✓ Branch 38 → 40 taken 372 times.
✓ Branch 41 → 42 taken 5731 times.
✓ Branch 41 → 43 taken 11395 times.
17126 if (token.literal || (!applyPositive(token.text, filter) && !applyNegated(token.text, filter)))
322
1/1
✓ Branch 42 → 43 taken 5731 times.
5731 remaining.append(token.text); // unknown "word:" tokens stay free text
323 13903 }
324
1/1
✓ Branch 49 → 50 taken 13903 times.
27806 return remaining.join(QLatin1Char(' '));
325 13903 }
326
327 1295 QString buildSearchQuery(const QString &freeText,
328 const MailCache::SearchFilter &filter) {
329 1295 QStringList parts;
330
331 // Free text first so parseSearchQuery() returns it verbatim as the
332 // remainder; excluded words belong to the text (D-14).
333
1/1
✓ Branch 2 → 3 taken 1295 times.
1295 const QString trimmedText = freeText.trimmed();
334
2/2
✓ Branch 4 → 5 taken 934 times.
✓ Branch 4 → 64 taken 361 times.
1295 if (!trimmedText.isEmpty()) {
335
1/1
✓ Branch 7 → 8 taken 934 times.
934 bool literal = trimmedText.contains(QLatin1Char('"'));
336 934 QStringList words;
337
5/5
✓ Branch 8 → 9 taken 934 times.
✓ Branch 9 → 10 taken 934 times.
✓ Branch 10 → 11 taken 934 times.
✓ Branch 25 → 12 taken 983 times.
✓ Branch 25 → 26 taken 934 times.
1917 for (const auto &token : tokenize(trimmedText)) {
338 983 MailCache::SearchFilter probe;
339
7/8
✓ Branch 14 → 15 taken 983 times.
✗ Branch 14 → 19 not taken.
✓ Branch 15 → 16 taken 983 times.
✓ Branch 16 → 17 taken 982 times.
✓ Branch 16 → 19 taken 1 time.
✓ Branch 17 → 18 taken 982 times.
✓ Branch 18 → 19 taken 1 time.
✓ Branch 18 → 20 taken 981 times.
983 literal |= token.literal || applyPositive(token.text, probe) || applyNegated(token.text, probe);
340
1/1
✓ Branch 21 → 22 taken 983 times.
983 words.append(token.text);
341 1917 }
342
2/2
✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 56 taken 933 times.
934 if (literal) {
343 1 QString escaped = trimmedText;
344
1/1
✓ Branch 34 → 35 taken 1 time.
1 escaped.replace(QLatin1Char('\\'), QStringLiteral("\\\\"));
345
1/1
✓ Branch 42 → 43 taken 1 time.
1 escaped.replace(QLatin1Char('"'), QStringLiteral("\\\""));
346
3/3
✓ Branch 49 → 50 taken 1 time.
✓ Branch 50 → 51 taken 1 time.
✓ Branch 51 → 52 taken 1 time.
1 parts.append(QLatin1Char('"') + escaped + QLatin1Char('"'));
347 1 } else {
348 // Unquoted words come back joined by one space, so the canonical form
349 // is that join (a tab or a run of spaces is not stable otherwise).
350
2/2
✓ Branch 58 → 59 taken 933 times.
✓ Branch 59 → 60 taken 933 times.
933 parts.append(words.join(QLatin1Char(' ')));
351 }
352 934 }
353
2/2
✓ Branch 79 → 66 taken 4 times.
✓ Branch 79 → 80 taken 1295 times.
1299 for (const QString &term : filter.excludedTerms)
354
4/5
✓ Branch 68 → 69 taken 4 times.
✗ Branch 68 → 77 not taken.
✓ Branch 69 → 70 taken 4 times.
✓ Branch 72 → 73 taken 4 times.
✓ Branch 73 → 74 taken 4 times.
4 if (!term.isEmpty()) parts.append(QLatin1Char('-') + quoteIfNeeded(term));
355
356 // Sprint 60 (Q2): one folder: token per pattern, order preserved so the
357 // parse(build(...)) round-trip yields the same list.
358
1/1
✓ Branch 80 → 81 taken 1295 times.
1295 appendEach(parts, "folder:", filter.folderPatterns);
359
2/2
✓ Branch 82 → 83 taken 180 times.
✓ Branch 82 → 94 taken 1115 times.
1295 if (!filter.fromFilter.isEmpty())
360
3/3
✓ Branch 83 → 84 taken 180 times.
✓ Branch 87 → 88 taken 180 times.
✓ Branch 88 → 89 taken 180 times.
360 parts.append(QStringLiteral("from:") + quoteIfNeeded(filter.fromFilter));
361
2/2
✓ Branch 95 → 96 taken 4 times.
✓ Branch 95 → 107 taken 1291 times.
1295 if (!filter.toFilter.isEmpty())
362
3/3
✓ Branch 96 → 97 taken 4 times.
✓ Branch 100 → 101 taken 4 times.
✓ Branch 101 → 102 taken 4 times.
8 parts.append(QStringLiteral("to:") + quoteIfNeeded(filter.toFilter));
363
2/2
✓ Branch 108 → 109 taken 4 times.
✓ Branch 108 → 120 taken 1291 times.
1295 if (!filter.listFilter.isEmpty())
364
3/3
✓ Branch 109 → 110 taken 4 times.
✓ Branch 113 → 114 taken 4 times.
✓ Branch 114 → 115 taken 4 times.
8 parts.append(QStringLiteral("list:") + quoteIfNeeded(filter.listFilter));
365
2/2
✓ Branch 121 → 122 taken 24 times.
✓ Branch 121 → 133 taken 1271 times.
1295 if (!filter.subjectFilter.isEmpty())
366
1/1
✓ Branch 127 → 128 taken 24 times.
48 parts.append(QStringLiteral("subject:") +
367
2/2
✓ Branch 122 → 123 taken 24 times.
✓ Branch 126 → 127 taken 24 times.
72 quoteIfNeeded(filter.subjectFilter));
368
2/2
✓ Branch 134 → 135 taken 1 time.
✓ Branch 134 → 146 taken 1294 times.
1295 if (!filter.aboutFilter.isEmpty())
369
3/3
✓ Branch 135 → 136 taken 1 time.
✓ Branch 139 → 140 taken 1 time.
✓ Branch 140 → 141 taken 1 time.
2 parts.append(QStringLiteral("about:") + quoteIfNeeded(filter.aboutFilter));
370
371 // S101.1.5: the relative word when the range came from one, else the
372 // shortest absolute form (2025, 2025-03, 2025-03-14, A..B, open ends).
373
8/8
✓ Branch 146 → 147 taken 1295 times.
✓ Branch 147 → 148 taken 1136 times.
✓ Branch 147 → 150 taken 159 times.
✓ Branch 148 → 149 taken 1136 times.
✓ Branch 149 → 150 taken 2 times.
✓ Branch 149 → 151 taken 1134 times.
✓ Branch 152 → 153 taken 161 times.
✓ Branch 152 → 169 taken 1134 times.
1295 if (filter.dateFrom.isValid() || filter.dateTo.isValid()) {
374
1/1
✓ Branch 163 → 164 taken 161 times.
322 parts.append(QStringLiteral("date:") +
375
3/3
✓ Branch 154 → 155 taken 5 times.
✓ Branch 154 → 156 taken 156 times.
✓ Branch 158 → 159 taken 156 times.
478 (!filter.datePreset.isEmpty() ? filter.datePreset
376
3/3
✓ Branch 156 → 157 taken 156 times.
✓ Branch 157 → 158 taken 156 times.
✓ Branch 162 → 163 taken 161 times.
478 : canonicalDateToken(filter.dateFrom.date(), filter.dateTo.date())));
377 }
378
379
1/1
✓ Branch 169 → 170 taken 1295 times.
1295 appendEach(parts, "tag:", filter.tags, true); // an empty label stays visible as tag:""
380
1/1
✓ Branch 170 → 171 taken 1295 times.
1295 appendTri(parts, filter.unread, "is:unread", "is:read");
381
1/1
✓ Branch 171 → 172 taken 1295 times.
1295 appendTri(parts, filter.flagged, "is:flagged", "is:unflagged");
382
1/1
✓ Branch 172 → 173 taken 1295 times.
1295 appendTri(parts, filter.answered, "is:answered", "is:unanswered");
383
1/1
✓ Branch 173 → 174 taken 1295 times.
1295 appendTri(parts, filter.hasAttachment, "has:attachment", "has:noattachment");
384
385
1/1
✓ Branch 174 → 175 taken 1295 times.
1295 appendEach(parts, "-folder:", filter.excludedFolders);
386
1/1
✓ Branch 175 → 176 taken 1295 times.
1295 appendEach(parts, "-from:", filter.excludedFrom);
387
1/1
✓ Branch 176 → 177 taken 1295 times.
1295 appendEach(parts, "-to:", filter.excludedTo);
388
1/1
✓ Branch 177 → 178 taken 1295 times.
1295 appendEach(parts, "-list:", filter.excludedLists);
389
1/1
✓ Branch 178 → 179 taken 1295 times.
1295 appendEach(parts, "-subject:", filter.excludedSubjects);
390
1/1
✓ Branch 179 → 180 taken 1295 times.
1295 appendEach(parts, "-tag:", filter.excludedTags);
391
392
1/1
✓ Branch 182 → 183 taken 1295 times.
2590 return parts.join(QLatin1Char(' '));
393 1295 }
394