• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gripmock / grpctestify-rust / 24368153097

13 Apr 2026 09:36PM UTC coverage: 75.096% (-0.3%) from 75.445%
24368153097

Pull #35

github

web-flow
Merge 97a02fd78 into 4ba0f08f1
Pull Request #35: feat: meta section & refactoring

2518 of 3592 new or added lines in 47 files covered. (70.1%)

155 existing lines in 9 files now uncovered.

16781 of 22346 relevant lines covered (75.1%)

2495.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.09
/src/optimizer/mod.rs
1
use serde::{Deserialize, Serialize};
2
use std::borrow::Cow;
3
use std::collections::{HashMap, HashSet};
4
use std::sync::LazyLock;
5

6
use crate::parser;
7
use crate::parser::assertions::strip_assertion_comments;
8
use crate::plugins::{PluginSignature, TypeInfo, extract_plugin_call_name};
9
use crate::utils::section_content_line;
10

11
fn likely_needs_assertion_rewrite(expr: &str) -> bool {
105✔
12
    expr.contains("==")
105✔
13
        || expr.contains("!=")
42✔
14
        || expr.contains('>')
37✔
15
        || expr.contains('<')
21✔
16
        || expr.contains(" startswith ")
21✔
17
        || expr.contains(" endswith ")
18✔
18
        || expr.contains("!!")
18✔
19
        || expr.contains("not not ")
15✔
20
        || expr.contains("if ")
14✔
21
        || expr.contains(" then ")
11✔
22
        || expr.contains(" else ")
11✔
23
        || expr.contains(" or ")
11✔
24
        || expr.contains(" and ")
10✔
25
        || expr.contains("@len(")
10✔
26
        || expr.contains(">= 0")
10✔
27
        || expr.contains("<= @")
10✔
28
        || expr.starts_with('(')
10✔
29
}
105✔
30

31
#[derive(Debug, Clone, Copy)]
32
struct RewriteRuleMetadata {
33
    id: &'static str,
34
    preconditions: &'static str,
35
    negative_cases: &'static str,
36
    proof_note: &'static str,
37
}
38

39
const REWRITE_RULES: &[RewriteRuleMetadata] = &[
40
    RewriteRuleMetadata {
41
        id: "OPT_B001",
42
        preconditions: "lhs is boolean plugin expr and rhs is true",
43
        negative_cases: "lhs is non-boolean, side-effectful, or unsafe-for-rewrite",
44
        proof_note: "Boolean identity: expr == true is equivalent to expr",
45
    },
46
    RewriteRuleMetadata {
47
        id: "OPT_B002",
48
        preconditions: "lhs is boolean plugin expr and rhs is false",
49
        negative_cases: "lhs is non-boolean, side-effectful, or unsafe-for-rewrite",
50
        proof_note: "Boolean negation: expr == false is equivalent to !expr",
51
    },
52
    RewriteRuleMetadata {
53
        id: "OPT_B003",
54
        preconditions: "lhs is true and rhs is boolean plugin expr",
55
        negative_cases: "rhs is non-boolean, side-effectful, or unsafe-for-rewrite",
56
        proof_note: "Boolean identity: true == expr is equivalent to expr",
57
    },
58
    RewriteRuleMetadata {
59
        id: "OPT_B004",
60
        preconditions: "lhs is false and rhs is boolean plugin expr",
61
        negative_cases: "rhs is non-boolean, side-effectful, or unsafe-for-rewrite",
62
        proof_note: "Boolean negation: false == expr is equivalent to !expr",
63
    },
64
    RewriteRuleMetadata {
65
        id: "OPT_B005",
66
        preconditions: "expression has form !!<bool-plugin-expr>",
67
        negative_cases: "inner expr is not proven boolean-safe",
68
        proof_note: "Double negation elimination for boolean expressions",
69
    },
70
    RewriteRuleMetadata {
71
        id: "OPT_B006",
72
        preconditions: "binary compare over two literals only",
73
        negative_cases: "contains non-literals, dynamic plugin calls, or unknown values",
74
        proof_note: "Constant folding preserves comparison result",
75
    },
76
    RewriteRuleMetadata {
77
        id: "OPT_B007",
78
        preconditions: "expression has form x == x and x is idempotent",
79
        negative_cases: "x may be non-idempotent or side-effectful",
80
        proof_note: "Reflexive equality over idempotent expressions is always true",
81
    },
82
    RewriteRuleMetadata {
83
        id: "OPT_B008",
84
        preconditions: "expression has form x != x and x is idempotent",
85
        negative_cases: "x may be non-idempotent or side-effectful",
86
        proof_note: "Reflexive inequality over idempotent expressions is always false",
87
    },
88
    RewriteRuleMetadata {
89
        id: "OPT_B013",
90
        preconditions: "lhs is boolean plugin expr and rhs is true",
91
        negative_cases: "lhs is non-boolean, side-effectful, or unsafe-for-rewrite",
92
        proof_note: "Boolean negation: expr != true is equivalent to !expr",
93
    },
94
    RewriteRuleMetadata {
95
        id: "OPT_B014",
96
        preconditions: "lhs is boolean plugin expr and rhs is false",
97
        negative_cases: "lhs is non-boolean, side-effectful, or unsafe-for-rewrite",
98
        proof_note: "Boolean identity: expr != false is equivalent to expr",
99
    },
100
    RewriteRuleMetadata {
101
        id: "OPT_B015",
102
        preconditions: "lhs is true and rhs is boolean plugin expr",
103
        negative_cases: "rhs is non-boolean, side-effectful, or unsafe-for-rewrite",
104
        proof_note: "Boolean negation: true != expr is equivalent to !expr",
105
    },
106
    RewriteRuleMetadata {
107
        id: "OPT_B016",
108
        preconditions: "lhs is false and rhs is boolean plugin expr",
109
        negative_cases: "rhs is non-boolean, side-effectful, or unsafe-for-rewrite",
110
        proof_note: "Boolean identity: false != expr is equivalent to expr",
111
    },
112
    RewriteRuleMetadata {
113
        id: "OPT_B017",
114
        preconditions: "expression has form not not <bool-plugin-expr>",
115
        negative_cases: "inner expr is not proven boolean-safe",
116
        proof_note: "Word-style double negation elimination",
117
    },
118
    RewriteRuleMetadata {
119
        id: "OPT_N001",
120
        preconditions: "operator alias startswith/endswith is present",
121
        negative_cases: "already canonicalized form",
122
        proof_note: "Canonical spelling rewrite preserves operator semantics",
123
    },
124
    RewriteRuleMetadata {
125
        id: "OPT_I001",
126
        preconditions: "if-then-else with boolean literal condition",
127
        negative_cases: "condition is not a literal true/false",
128
        proof_note: "Dead branch elimination: if true then A else B end = A",
129
    },
130
    RewriteRuleMetadata {
131
        id: "OPT_I002",
132
        preconditions: "if-then-else with identical then/else branches",
133
        negative_cases: "branches are different expressions",
134
        proof_note: "Branch merging: if C then X else X end = X",
135
    },
136
    RewriteRuleMetadata {
137
        id: "OPT_I003",
138
        preconditions: "nested if with redundant condition check",
139
        negative_cases: "conditions are not related",
140
        proof_note: "Condition simplification for nested boolean expressions",
141
    },
142
    RewriteRuleMetadata {
143
        id: "OPT_I004",
144
        preconditions: "if-then-else with boolean condition and literal branches",
145
        negative_cases: "branches are not boolean literals",
146
        proof_note: "Boolean simplification: if C then true else false end = C",
147
    },
148
    RewriteRuleMetadata {
149
        id: "OPT_I005",
150
        preconditions: "if-then-else with negated condition pattern",
151
        negative_cases: "branches don't match negation pattern",
152
        proof_note: "Condition inversion: if C then false else true end = !C",
153
    },
154
    RewriteRuleMetadata {
155
        id: "OPT_B009",
156
        preconditions: "boolean expression OR true/false",
157
        negative_cases: "operand is not boolean literal",
158
        proof_note: "Boolean identity: A or true = true, A or false = A",
159
    },
160
    RewriteRuleMetadata {
161
        id: "OPT_B010",
162
        preconditions: "boolean expression AND true/false",
163
        negative_cases: "operand is not boolean literal",
164
        proof_note: "Boolean absorption: A and true = A, A and false = false",
165
    },
166
    RewriteRuleMetadata {
167
        id: "OPT_P001",
168
        preconditions: "@len(expr) compared to zero",
169
        negative_cases: "comparison is not with zero or not @len plugin",
170
        proof_note: "Length check simplification: @len(x) == 0 = @empty(x)",
171
    },
172
    RewriteRuleMetadata {
173
        id: "OPT_P002",
174
        preconditions: "expression wrapped in outer parentheses only",
175
        negative_cases: "inner expression has internal parentheses (ambiguity risk)",
176
        proof_note: "Redundant parentheses removal: (expr) = expr",
177
    },
178
    RewriteRuleMetadata {
179
        id: "OPT_N002",
180
        preconditions: "negation of comparison operator",
181
        negative_cases: "inner expression is not a comparison",
182
        proof_note: "Comparison negation: not (A == B) = A != B",
183
    },
184
    RewriteRuleMetadata {
185
        id: "OPT_T001",
186
        preconditions: "UInt-returning plugin (e.g. @len) compared with 0 via >= or <=",
187
        negative_cases: "plugin does not return UInt or comparison is not with 0",
188
        proof_note: "Unsigned integers are always >= 0, so @uint() >= 0 = true",
189
    },
190
];
191

192
fn rule_metadata(rule_id: &str) -> Option<&'static RewriteRuleMetadata> {
87✔
193
    REWRITE_RULES.iter().find(|r| r.id == rule_id)
925✔
194
}
87✔
195

196
#[derive(Debug, Clone, Serialize, Deserialize)]
197
pub struct OptimizationHint {
198
    pub rule_id: String,
199
    pub line: usize,
200
    pub before: String,
201
    pub after: String,
202
    #[serde(skip_serializing_if = "Option::is_none")]
203
    pub preconditions: Option<String>,
204
    #[serde(skip_serializing_if = "Option::is_none")]
205
    pub negative_cases: Option<String>,
206
    #[serde(skip_serializing_if = "Option::is_none")]
207
    pub proof_note: Option<String>,
208
}
209

210
fn build_hint(rule_id: &str, line: usize, before: &str, after: String) -> OptimizationHint {
31✔
211
    let meta = rule_metadata(rule_id);
31✔
212
    OptimizationHint {
213
        rule_id: rule_id.to_string(),
31✔
214
        line,
31✔
215
        before: before.to_string(),
31✔
216
        after,
31✔
217
        preconditions: meta.map(|m| m.preconditions.to_string()),
31✔
218
        negative_cases: meta.map(|m| m.negative_cases.to_string()),
31✔
219
        proof_note: meta.map(|m| m.proof_note.to_string()),
31✔
220
    }
221
}
31✔
222

223
use crate::plugins::PLUGIN_SIGNATURES;
224

225
static BOOLEAN_PLUGINS: LazyLock<HashSet<String>> = LazyLock::new(|| {
32✔
226
    PLUGIN_SIGNATURES
32✔
227
        .iter()
32✔
228
        .filter(|(_, signature)| {
544✔
229
            signature.return_type == TypeInfo::Bool
544✔
230
                && signature.safe_for_rewrite
288✔
231
                && signature.deterministic
288✔
232
                && signature.idempotent
288✔
233
        })
544✔
234
        .map(|(name, _)| name.clone())
288✔
235
        .collect()
32✔
236
});
32✔
237

238
fn plugin_signatures() -> &'static HashMap<String, PluginSignature> {
217✔
239
    &PLUGIN_SIGNATURES
217✔
240
}
217✔
241

242
fn boolean_plugins() -> &'static HashSet<String> {
154✔
243
    &BOOLEAN_PLUGINS
154✔
244
}
154✔
245

246
fn is_boolean_plugin_expr(expr: &str, bool_plugins: &HashSet<String>) -> bool {
142✔
247
    let Some(plugin_name) = extract_plugin_call_name(expr) else {
142✔
248
        return false;
96✔
249
    };
250

251
    bool_plugins.contains(plugin_name.as_str())
46✔
252
}
142✔
253

254
fn suggest_boolean_rewrite(
96✔
255
    expr: &str,
96✔
256
    bool_plugins: &HashSet<String>,
96✔
257
) -> Option<(&'static str, String)> {
96✔
258
    let (lhs, rhs) = expr.split_once("==")?;
96✔
259
    let lhs = lhs.trim();
63✔
260
    let rhs = rhs.trim();
63✔
261

262
    if is_boolean_plugin_expr(lhs, bool_plugins) && rhs == "true" {
63✔
263
        return Some(("OPT_B001", lhs.to_string()));
6✔
264
    }
57✔
265
    if is_boolean_plugin_expr(lhs, bool_plugins) && rhs == "false" {
57✔
266
        return Some(("OPT_B002", format!("!{}", lhs)));
1✔
267
    }
56✔
268
    if lhs == "true" && is_boolean_plugin_expr(rhs, bool_plugins) {
56✔
269
        return Some(("OPT_B003", rhs.to_string()));
3✔
270
    }
53✔
271
    if lhs == "false" && is_boolean_plugin_expr(rhs, bool_plugins) {
53✔
272
        return Some(("OPT_B004", format!("!{}", rhs)));
1✔
273
    }
52✔
274

275
    None
52✔
276
}
96✔
277

278
fn suggest_not_not_rewrite(
85✔
279
    expr: &str,
85✔
280
    bool_plugins: &HashSet<String>,
85✔
281
) -> Option<(&'static str, String)> {
85✔
282
    let trimmed = expr.trim();
85✔
283
    if !trimmed.starts_with("not not ") {
85✔
284
        return None;
84✔
285
    }
1✔
286

287
    let inner = trimmed[8..].trim();
1✔
288
    if is_boolean_plugin_expr(inner, bool_plugins) {
1✔
289
        return Some(("OPT_B017", inner.to_string()));
1✔
290
    }
×
291

292
    None
×
293
}
85✔
294

295
fn suggest_inequality_rewrite(
84✔
296
    expr: &str,
84✔
297
    bool_plugins: &HashSet<String>,
84✔
298
) -> Option<(&'static str, String)> {
84✔
299
    let (lhs, rhs) = expr.split_once("!=")?;
84✔
300
    let lhs = lhs.trim();
5✔
301
    let rhs = rhs.trim();
5✔
302

303
    if is_boolean_plugin_expr(lhs, bool_plugins) && rhs == "true" {
5✔
304
        return Some(("OPT_B013", format!("!{}", lhs)));
1✔
305
    }
4✔
306
    if is_boolean_plugin_expr(lhs, bool_plugins) && rhs == "false" {
4✔
307
        return Some(("OPT_B014", lhs.to_string()));
1✔
308
    }
3✔
309
    if lhs == "true" && is_boolean_plugin_expr(rhs, bool_plugins) {
3✔
310
        return Some(("OPT_B015", format!("!{}", rhs)));
1✔
311
    }
2✔
312
    if lhs == "false" && is_boolean_plugin_expr(rhs, bool_plugins) {
2✔
313
        return Some(("OPT_B016", rhs.to_string()));
1✔
314
    }
1✔
315

316
    None
1✔
317
}
84✔
318

319
/// Redundant parentheses: (expr) -> expr (single expression, no ambiguity)
320
fn suggest_redundant_parens(expr: &str) -> Option<(&'static str, String)> {
71✔
321
    let trimmed = expr.trim();
71✔
322
    if !trimmed.starts_with('(') || !trimmed.ends_with(')') {
71✔
323
        return None;
70✔
324
    }
1✔
325

326
    let inner = &trimmed[1..trimmed.len() - 1].trim();
1✔
327
    if inner.is_empty() {
1✔
328
        return None;
×
329
    }
1✔
330

331
    let balanced = inner.chars().fold(0i32, |acc, c| {
16✔
332
        if c == '(' {
16✔
333
            acc + 1
1✔
334
        } else if c == ')' {
15✔
335
            acc - 1
1✔
336
        } else {
337
            acc
14✔
338
        }
339
    });
16✔
340
    if balanced != 0 {
1✔
341
        return None;
×
342
    }
1✔
343

344
    Some(("OPT_P002", inner.to_string()))
1✔
345
}
71✔
346

347
fn suggest_double_negation_rewrite(
80✔
348
    expr: &str,
80✔
349
    bool_plugins: &HashSet<String>,
80✔
350
) -> Option<(&'static str, String)> {
80✔
351
    let trimmed = expr.trim();
80✔
352
    if !trimmed.starts_with("!!") {
80✔
353
        return None;
77✔
354
    }
3✔
355

356
    let inner = trimmed[2..].trim();
3✔
357
    if is_boolean_plugin_expr(inner, bool_plugins) {
3✔
358
        return Some(("OPT_B005", inner.to_string()));
2✔
359
    }
1✔
360

361
    None
1✔
362
}
80✔
363

364
fn suggest_operator_canonicalization(expr: &str) -> Option<(&'static str, String)> {
78✔
365
    if expr.contains(" startswith ") {
78✔
366
        let rewritten = expr.replace(" startswith ", " startsWith ");
2✔
367
        return Some(("OPT_N001", rewritten));
2✔
368
    }
76✔
369
    if expr.contains(" endswith ") {
76✔
370
        let rewritten = expr.replace(" endswith ", " endsWith ");
×
371
        return Some(("OPT_N001", rewritten));
×
372
    }
76✔
373
    None
76✔
374
}
78✔
375

376
fn parse_literal(expr: &str) -> Option<serde_json::Value> {
83✔
377
    let trimmed = expr.trim();
83✔
378
    if trimmed.is_empty() {
83✔
379
        return None;
×
380
    }
83✔
381

382
    if trimmed == "true" {
83✔
383
        return Some(serde_json::Value::Bool(true));
×
384
    }
83✔
385
    if trimmed == "false" {
83✔
386
        return Some(serde_json::Value::Bool(false));
×
387
    }
83✔
388
    if trimmed == "null" {
83✔
389
        return Some(serde_json::Value::Null);
×
390
    }
83✔
391

392
    if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
83✔
393
        return serde_json::from_str(trimmed).ok();
5✔
394
    }
78✔
395

396
    if let Ok(i) = trimmed.parse::<i64>() {
78✔
397
        return Some(serde_json::Value::Number(serde_json::Number::from(i)));
5✔
398
    }
73✔
399

400
    if let Ok(f) = trimmed.parse::<f64>() {
73✔
401
        return serde_json::Number::from_f64(f).map(serde_json::Value::Number);
×
402
    }
73✔
403

404
    None
73✔
405
}
83✔
406

407
fn suggest_constant_folding(expr: &str) -> Option<(&'static str, String)> {
79✔
408
    let operators = ["==", "!=", ">=", "<=", ">", "<"];
79✔
409
    for op in operators {
457✔
410
        let Some(idx) = expr.find(op) else {
457✔
411
            continue;
385✔
412
        };
413

414
        let lhs_raw = expr[..idx].trim();
72✔
415
        let rhs_raw = expr[idx + op.len()..].trim();
72✔
416
        if lhs_raw.is_empty() || rhs_raw.is_empty() {
72✔
417
            continue;
×
418
        }
72✔
419

420
        let Some(lhs) = parse_literal(lhs_raw) else {
72✔
421
            continue;
67✔
422
        };
423
        let Some(rhs) = parse_literal(rhs_raw) else {
5✔
424
            continue;
×
425
        };
426

427
        let folded = match op {
5✔
428
            "==" => Some(lhs == rhs),
5✔
429
            "!=" => Some(lhs != rhs),
2✔
430
            ">" | "<" | ">=" | "<=" => compare_literal_numbers(&lhs, &rhs, op),
2✔
431
            _ => None,
×
432
        }?;
×
433

434
        return Some(("OPT_B006", folded.to_string()));
5✔
435
    }
436

437
    None
74✔
438
}
79✔
439

440
fn compare_literal_numbers(
2✔
441
    lhs: &serde_json::Value,
2✔
442
    rhs: &serde_json::Value,
2✔
443
    op: &str,
2✔
444
) -> Option<bool> {
2✔
445
    let lhs_num = lhs.as_number()?;
2✔
446
    let rhs_num = rhs.as_number()?;
2✔
447

448
    let lhs_i = lhs_num
2✔
449
        .as_i64()
2✔
450
        .map(i128::from)
2✔
451
        .or_else(|| lhs_num.as_u64().map(i128::from));
2✔
452
    let rhs_i = rhs_num
2✔
453
        .as_i64()
2✔
454
        .map(i128::from)
2✔
455
        .or_else(|| rhs_num.as_u64().map(i128::from));
2✔
456

457
    if let (Some(l), Some(r)) = (lhs_i, rhs_i) {
2✔
458
        return Some(match op {
2✔
459
            ">" => l > r,
2✔
460
            "<" => l < r,
×
461
            ">=" => l >= r,
×
462
            "<=" => l <= r,
×
463
            _ => unreachable!(),
×
464
        });
465
    }
×
466

467
    let (l, r) = (lhs_num.as_f64()?, rhs_num.as_f64()?);
×
468
    Some(match op {
×
469
        ">" => l > r,
×
470
        "<" => l < r,
×
471
        ">=" => l >= r,
×
472
        "<=" => l <= r,
×
473
        _ => unreachable!(),
×
474
    })
475
}
2✔
476

477
fn is_idempotent_expr(expr: &str, signatures: &HashMap<String, PluginSignature>) -> bool {
3✔
478
    let trimmed = expr.trim();
3✔
479
    if trimmed.is_empty() {
3✔
480
        return false;
×
481
    }
3✔
482

483
    if parse_literal(trimmed).is_some() {
3✔
484
        return true;
×
485
    }
3✔
486

487
    if (trimmed.starts_with("{{") && trimmed.ends_with("}}")) || trimmed.starts_with('.') {
3✔
488
        return true;
2✔
489
    }
1✔
490

491
    if trimmed.starts_with('(') && trimmed.ends_with(')') && trimmed.len() >= 2 {
1✔
492
        return is_idempotent_expr(&trimmed[1..trimmed.len() - 1], signatures);
×
493
    }
1✔
494

495
    if let Some(plugin_name) = extract_plugin_call_name(trimmed) {
1✔
496
        return signatures
1✔
497
            .get(plugin_name.as_str())
1✔
498
            .map(|sig| sig.idempotent)
1✔
499
            .unwrap_or(false);
1✔
500
    }
×
501

502
    false
×
503
}
3✔
504

505
fn suggest_reflexive_idempotent(
73✔
506
    expr: &str,
73✔
507
    signatures: &HashMap<String, PluginSignature>,
73✔
508
) -> Option<(&'static str, String)> {
73✔
509
    let (_op, lhs, rhs, rule_id, result) = if let Some((l, r)) = expr.split_once("==") {
73✔
510
        ("==", l, r, "OPT_B007", "true")
51✔
511
    } else if let Some((l, r)) = expr.split_once("!=") {
22✔
512
        ("!=", l, r, "OPT_B008", "false")
1✔
513
    } else {
514
        return None;
21✔
515
    };
516

517
    let lhs = lhs.trim();
52✔
518
    let rhs = rhs.trim();
52✔
519

520
    if lhs.is_empty() || rhs.is_empty() || lhs != rhs {
52✔
521
        return None;
49✔
522
    }
3✔
523

524
    if parse_literal(lhs).is_some() && parse_literal(rhs).is_some() {
3✔
525
        return None;
×
526
    }
3✔
527

528
    if !is_idempotent_expr(lhs, signatures) {
3✔
529
        return None;
1✔
530
    }
2✔
531

532
    Some((rule_id, result.to_string()))
2✔
533
}
73✔
534

535
/// Parse if-then-else expression and extract parts
536
fn parse_if_then_else(expr: &str) -> Option<(&str, &str, &str)> {
355✔
537
    let expr = expr.trim();
355✔
538

539
    if !expr.starts_with("if ") {
355✔
540
        return None;
332✔
541
    }
23✔
542

543
    let bytes = expr.as_bytes();
23✔
544
    let mut paren_depth = 0;
23✔
545
    let mut if_depth = 0;
23✔
546
    let mut then_pos = None;
23✔
547

548
    let mut i = 0;
23✔
549
    let mut in_string = false;
23✔
550
    let mut string_char = None;
23✔
551
    while i < bytes.len() {
309✔
552
        // Handle string literals
553
        if in_string {
309✔
554
            if bytes[i] == string_char.unwrap() && (i == 0 || bytes[i - 1] != b'\\') {
13✔
555
                in_string = false;
5✔
556
            }
8✔
557
            i += 1;
13✔
558
            continue;
13✔
559
        }
296✔
560
        if bytes[i] == b'"' || bytes[i] == b'\'' {
296✔
561
            in_string = true;
5✔
562
            string_char = Some(bytes[i]);
5✔
563
            i += 1;
5✔
564
            continue;
5✔
565
        }
291✔
566

567
        match &bytes[i..i + 1] {
291✔
568
            b"(" => paren_depth += 1,
291✔
569
            b")" => paren_depth -= 1,
4✔
570
            _ => {}
283✔
571
        }
572

573
        if paren_depth == 0 && i + 3 <= bytes.len() && &bytes[i..i + 3] == b"if " {
291✔
574
            if_depth += 1;
23✔
575
        }
268✔
576

577
        if paren_depth == 0
291✔
578
            && if_depth == 1
287✔
579
            && i + 6 <= bytes.len()
287✔
580
            && &bytes[i..i + 6] == b" then "
287✔
581
        {
582
            then_pos = Some(i);
23✔
583
            break;
23✔
584
        }
268✔
585

586
        i += 1;
268✔
587
    }
588

589
    let then_pos = then_pos?;
23✔
590
    let condition = expr[3..then_pos].trim();
23✔
591

592
    let rest = &expr[then_pos + 6..];
23✔
593
    let bytes = rest.as_bytes();
23✔
594
    let mut else_pos = None;
23✔
595
    let mut nested_if = 0;
23✔
596
    paren_depth = 0;
23✔
597

598
    let mut in_string = false;
23✔
599
    let mut string_char = None;
23✔
600

601
    i = 0;
23✔
602
    while i < bytes.len() {
216✔
603
        if in_string {
216✔
604
            if bytes[i] == string_char.unwrap() && (i == 0 || bytes[i - 1] != b'\\') {
75✔
605
                in_string = false;
14✔
606
            }
61✔
607
            i += 1;
75✔
608
            continue;
75✔
609
        }
141✔
610
        if bytes[i] == b'"' || bytes[i] == b'\'' {
141✔
611
            in_string = true;
14✔
612
            string_char = Some(bytes[i]);
14✔
613
            i += 1;
14✔
614
            continue;
14✔
615
        }
127✔
616

617
        match &bytes[i..i + 1] {
127✔
618
            b"(" => paren_depth += 1,
127✔
619
            b")" => paren_depth -= 1,
2✔
620
            _ => {}
123✔
621
        }
622

623
        if paren_depth == 0 && i + 3 <= bytes.len() && &bytes[i..i + 3] == b"if " {
127✔
624
            nested_if += 1;
×
625
        }
127✔
626

627
        if paren_depth == 0 && i + 6 <= bytes.len() && &bytes[i..i + 6] == b" else " {
127✔
628
            if nested_if == 0 {
23✔
629
                else_pos = Some(i);
23✔
630
                break;
23✔
631
            }
×
632
            nested_if -= 1;
×
633
        }
104✔
634

635
        i += 1;
104✔
636
    }
637

638
    let else_pos = else_pos?;
23✔
639
    let then_expr = rest[..else_pos].trim();
23✔
640

641
    let else_and_end = &rest[else_pos + 6..];
23✔
642
    let else_expr = else_and_end.strip_suffix(" end")?.trim();
23✔
643

644
    Some((condition, then_expr, else_expr))
23✔
645
}
355✔
646

647
/// Dead branch elimination: if true then A else B = A
648
fn suggest_dead_branch_elimination(expr: &str) -> Option<(&'static str, String)> {
72✔
649
    let (condition, then_expr, else_expr) = parse_if_then_else(expr)?;
72✔
650

651
    if condition == "true" {
6✔
652
        return Some(("OPT_I001", then_expr.to_string()));
2✔
653
    }
4✔
654

655
    if condition == "false" {
4✔
656
        return Some(("OPT_I001", else_expr.to_string()));
1✔
657
    }
3✔
658

659
    None
3✔
660
}
72✔
661

662
/// Branch merging: if C then X else X = X
663
fn suggest_branch_merging(expr: &str) -> Option<(&'static str, String)> {
70✔
664
    let (_condition, then_expr, else_expr) = parse_if_then_else(expr)?;
70✔
665

666
    if then_expr == else_expr {
4✔
667
        return Some(("OPT_I002", then_expr.to_string()));
2✔
668
    }
2✔
669

670
    None
2✔
671
}
70✔
672

673
/// Nested if simplification: if A then (if A then X else Y) else Z = if A then X else Z
674
fn suggest_nested_if_simplification(expr: &str) -> Option<(&'static str, String)> {
69✔
675
    let (outer_cond, inner_expr, else_expr) = parse_if_then_else(expr)?;
69✔
676

677
    // Strip parentheses from inner expression if present
678
    let inner_stripped = inner_expr.trim();
3✔
679
    let inner_stripped = if inner_stripped.starts_with('(') && inner_stripped.ends_with(')') {
3✔
680
        &inner_stripped[1..inner_stripped.len() - 1]
1✔
681
    } else {
682
        inner_stripped
2✔
683
    };
684

685
    let (inner_cond, inner_then, _inner_else) = parse_if_then_else(inner_stripped)?;
3✔
686

687
    if outer_cond == inner_cond {
1✔
688
        let result = format!(
1✔
689
            "if {} then {} else {} end",
690
            outer_cond, inner_then, else_expr
691
        );
692
        return Some(("OPT_I003", result));
1✔
693
    }
×
694

695
    None
×
696
}
69✔
697

698
/// Boolean simplification: if C then true else false = C
699
fn suggest_boolean_simplification(expr: &str) -> Option<(&'static str, String)> {
69✔
700
    let (condition, then_expr, else_expr) = parse_if_then_else(expr)?;
69✔
701

702
    if then_expr == "true" && else_expr == "false" {
3✔
703
        return Some(("OPT_I004", condition.to_string()));
2✔
704
    }
1✔
705

706
    None
1✔
707
}
69✔
708

709
/// Condition inversion: if C then false else true = !C
710
fn suggest_condition_inversion(expr: &str) -> Option<(&'static str, String)> {
68✔
711
    let (condition, then_expr, else_expr) = parse_if_then_else(expr)?;
68✔
712

713
    if then_expr == "false" && else_expr == "true" {
2✔
714
        return Some(("OPT_I005", format!("!{}", condition)));
2✔
715
    }
×
716

717
    None
×
718
}
68✔
719

720
/// Boolean identity/absorption: A or true = true, A and false = false
721
fn suggest_boolean_identity_laws(expr: &str) -> Option<(&'static str, String)> {
72✔
722
    let expr = expr.trim();
72✔
723

724
    // Check for "or true" / "or false"
725
    if let Some(or_pos) = expr.find(" or ") {
72✔
726
        let left = expr[..or_pos].trim();
4✔
727
        let right = expr[or_pos + 4..].trim();
4✔
728

729
        if right == "true" || left == "true" {
4✔
730
            return Some(("OPT_B009", "true".to_string()));
3✔
731
        }
1✔
732
        if right == "false" {
1✔
733
            return Some(("OPT_B009", left.to_string()));
1✔
734
        }
×
735
        if left == "false" {
×
736
            return Some(("OPT_B009", right.to_string()));
×
737
        }
×
738
    }
68✔
739

740
    // Check for "and true" / "and false"
741
    if let Some(and_pos) = expr.find(" and ") {
68✔
742
        let left = expr[..and_pos].trim();
3✔
743
        let right = expr[and_pos + 5..].trim();
3✔
744

745
        if left == "true" {
3✔
746
            return Some(("OPT_B010", right.to_string()));
×
747
        }
3✔
748
        if right == "true" {
3✔
749
            return Some(("OPT_B010", left.to_string()));
1✔
750
        }
2✔
751
        if left == "false" || right == "false" {
2✔
752
            return Some(("OPT_B010", "false".to_string()));
2✔
753
        }
×
754
    }
65✔
755

756
    None
65✔
757
}
72✔
758

759
/// Plugin-specific: @len(.x) == 0 → @empty(.x)
760
fn suggest_plugin_length_simplification(expr: &str) -> Option<(&'static str, String)> {
69✔
761
    let expr = expr.trim();
69✔
762

763
    // Patterns: @len(.x) == 0, @len(.x) != 0, @len(.x) > 0
764
    let operators = [
69✔
765
        (" == ", "=="),
69✔
766
        (" != ", "!="),
69✔
767
        (" > ", ">"),
69✔
768
        (" < ", "<"),
69✔
769
        (" <= ", "<="),
69✔
770
    ];
69✔
771

772
    for (op_str, op_name) in operators {
308✔
773
        if let Some(op_pos) = expr.find(op_str) {
308✔
774
            let left = expr[..op_pos].trim();
64✔
775
            let right = expr[op_pos + op_str.len()..].trim();
64✔
776

777
            // Check if left side is @len(...) and right side is 0
778
            if left.starts_with("@len(") && left.ends_with(')') && right == "0" {
64✔
779
                let inner = &left[5..left.len() - 1]; // Extract content inside @len(...)
14✔
780

781
                if op_name == "==" {
14✔
782
                    return Some(("OPT_P001", format!("@empty({})", inner)));
2✔
783
                }
12✔
784
                if op_name == "!=" {
12✔
785
                    return Some(("OPT_P001", format!("@len({}) > 0", inner)));
1✔
786
                }
11✔
787
                if op_name == ">" {
11✔
788
                    // @len(.x) > 0 is already optimal, no simplification
789
                    return None;
11✔
790
                }
×
791
                if op_name == "<" {
×
792
                    // @len(.x) < 0 is always false (length can't be negative)
793
                    return Some(("OPT_P001", "false".to_string()));
×
794
                }
×
795
                if op_name == "<=" {
×
796
                    // @len(.x) <= 0 means empty (length is non-negative)
797
                    return Some(("OPT_P001", format!("@empty({})", inner)));
×
798
                }
×
799
            }
50✔
800

801
            // Check reverse: 0 == @len(.x)
802
            if right.starts_with("@len(") && right.ends_with(')') && left == "0" {
50✔
803
                let inner = &right[5..right.len() - 1];
1✔
804

805
                if op_name == "==" {
1✔
806
                    return Some(("OPT_P001", format!("@empty({})", inner)));
1✔
807
                }
×
808
                if op_name == "!=" {
×
809
                    return Some(("OPT_P001", format!("@len({}) > 0", inner)));
×
810
                }
×
811
                if op_name == ">" {
×
812
                    return Some(("OPT_P001", "false".to_string()));
×
813
                }
×
814
                if op_name == "<" {
×
815
                    return None;
×
816
                }
×
817
                if op_name == "<=" {
×
818
                    return Some(("OPT_P001", format!("@empty({})", inner)));
×
819
                }
×
820
            }
49✔
821
        }
244✔
822
    }
823

824
    None
54✔
825
}
69✔
826

827
/// Type-aware numeric comparison optimization.
828
/// Uses TypeInfo to detect that certain plugins return unsigned integers,
829
/// making comparisons like `@len(.x) >= 0` always true.
830
fn suggest_type_aware_numeric_comparison(expr: &str) -> Option<(&'static str, String)> {
64✔
831
    let signatures = plugin_signatures();
64✔
832
    let trimmed = expr.trim();
64✔
833

834
    let (left, right) = if let Some(idx) = trimmed.find(">=") {
64✔
835
        (trimmed[..idx].trim(), trimmed[idx + 2..].trim())
1✔
836
    } else if let Some(idx) = trimmed.find("<=") {
63✔
NEW
837
        (trimmed[..idx].trim(), trimmed[idx + 2..].trim())
×
838
    } else {
839
        return None;
63✔
840
    };
841

842
    let plugin_call = if right == "0" {
1✔
843
        left
1✔
NEW
844
    } else if left == "0" {
×
NEW
845
        right
×
846
    } else {
NEW
847
        return None;
×
848
    };
849

850
    if let Some(plugin_name) = extract_plugin_call_name(plugin_call)
1✔
851
        && let Some(sig) = signatures.get(plugin_name.as_str())
1✔
852
        && sig.return_type == TypeInfo::UInt
1✔
853
    {
854
        Some(("OPT_T001", "true".to_string()))
1✔
855
    } else {
NEW
856
        None
×
857
    }
858
}
64✔
859

860
/// Comparison negation: not (.x == 5) → .x != 5
861
fn suggest_comparison_negation(expr: &str) -> Option<(&'static str, String)> {
67✔
862
    let expr = expr.trim();
67✔
863

864
    if !expr.starts_with("not (") || !expr.ends_with(')') {
67✔
865
        return None;
62✔
866
    }
5✔
867

868
    let inner = expr[5..expr.len() - 1].trim();
5✔
869

870
    // Comparison operators to negate
871
    let negations = [
5✔
872
        (" == ", " != "),
5✔
873
        (" != ", " == "),
5✔
874
        (" > ", " <= "),
5✔
875
        (" < ", " >= "),
5✔
876
        (" >= ", " < "),
5✔
877
        (" <= ", " > "),
5✔
878
    ];
5✔
879

880
    for (op, neg_op) in negations {
12✔
881
        if let Some(op_pos) = inner.find(op) {
12✔
882
            let left = inner[..op_pos].trim();
5✔
883
            let right = inner[op_pos + op.len()..].trim();
5✔
884

885
            if !left.is_empty() && !right.is_empty() {
5✔
886
                return Some(("OPT_N002", format!("{}{}{}", left, neg_op, right)));
5✔
887
            }
×
888
        }
7✔
889
    }
890

891
    None
×
892
}
67✔
893

894
fn rewrite_assertion_expression_with_context(
96✔
895
    expr: &str,
96✔
896
    signatures: &HashMap<String, PluginSignature>,
96✔
897
    bool_plugins: &HashSet<String>,
96✔
898
) -> Option<(&'static str, String)> {
96✔
899
    if let Some((rule_id, rewrite)) = suggest_boolean_rewrite(expr, bool_plugins) {
96✔
900
        return Some((rule_id, rewrite));
11✔
901
    }
85✔
902

903
    if let Some((rule_id, rewrite)) = suggest_not_not_rewrite(expr, bool_plugins) {
85✔
904
        return Some((rule_id, rewrite));
1✔
905
    }
84✔
906

907
    if let Some((rule_id, rewrite)) = suggest_inequality_rewrite(expr, bool_plugins) {
84✔
908
        return Some((rule_id, rewrite));
4✔
909
    }
80✔
910

911
    if let Some((rule_id, rewrite)) = suggest_double_negation_rewrite(expr, bool_plugins) {
80✔
912
        return Some((rule_id, rewrite));
2✔
913
    }
78✔
914

915
    if let Some((rule_id, rewrite)) = suggest_operator_canonicalization(expr) {
78✔
916
        return Some((rule_id, rewrite));
2✔
917
    }
76✔
918

919
    if let Some((rule_id, rewrite)) = suggest_constant_folding(expr) {
76✔
920
        return Some((rule_id, rewrite));
3✔
921
    }
73✔
922

923
    if let Some((rule_id, rewrite)) = suggest_reflexive_idempotent(expr, signatures) {
73✔
924
        return Some((rule_id, rewrite));
2✔
925
    }
71✔
926

927
    // Redundant parentheses removal
928
    if let Some((rule_id, rewrite)) = suggest_redundant_parens(expr) {
71✔
929
        return Some((rule_id, rewrite));
1✔
930
    }
70✔
931

932
    // If-then-else optimizations
933
    if let Some((rule_id, rewrite)) = suggest_dead_branch_elimination(expr) {
70✔
934
        return Some((rule_id, rewrite));
1✔
935
    }
69✔
936

937
    if let Some((rule_id, rewrite)) = suggest_branch_merging(expr) {
69✔
938
        return Some((rule_id, rewrite));
1✔
939
    }
68✔
940

941
    if let Some((rule_id, rewrite)) = suggest_nested_if_simplification(expr) {
68✔
942
        return Some((rule_id, rewrite));
×
943
    }
68✔
944

945
    if let Some((rule_id, rewrite)) = suggest_boolean_simplification(expr) {
68✔
946
        return Some((rule_id, rewrite));
1✔
947
    }
67✔
948

949
    if let Some((rule_id, rewrite)) = suggest_condition_inversion(expr) {
67✔
950
        return Some((rule_id, rewrite));
1✔
951
    }
66✔
952

953
    // Boolean identity/absorption laws
954
    if let Some((rule_id, rewrite)) = suggest_boolean_identity_laws(expr) {
66✔
955
        return Some((rule_id, rewrite));
1✔
956
    }
65✔
957

958
    // Plugin-specific optimizations
959
    if let Some((rule_id, rewrite)) = suggest_plugin_length_simplification(expr) {
65✔
960
        return Some((rule_id, rewrite));
1✔
961
    }
64✔
962

963
    // Type-aware optimizations based on TypeInfo
964
    if let Some((rule_id, rewrite)) = suggest_type_aware_numeric_comparison(expr) {
64✔
965
        return Some((rule_id, rewrite));
1✔
966
    }
63✔
967

968
    // Comparison negation normalization
969
    suggest_comparison_negation(expr)
63✔
970
}
96✔
971

972
pub fn rewrite_assertion_expression(expr: &str) -> Option<(&'static str, String)> {
×
973
    let signatures = plugin_signatures();
×
974
    let bool_plugins = boolean_plugins();
×
975
    rewrite_assertion_expression_with_context(expr, signatures, bool_plugins)
×
976
}
×
977

978
pub fn rewrite_assertion_expression_fixed_point(expr: &str) -> String {
45✔
979
    let signatures = plugin_signatures();
45✔
980
    let bool_plugins = boolean_plugins();
45✔
981

982
    let mut current = Cow::Borrowed(expr.trim());
45✔
983
    for _ in 0..32 {
45✔
984
        let Some((_, rewritten)) =
3✔
985
            rewrite_assertion_expression_with_context(&current, signatures, bool_plugins)
48✔
986
        else {
987
            break;
45✔
988
        };
989

990
        let normalized = rewritten.trim();
3✔
991
        if normalized == current.as_ref() {
3✔
992
            break;
×
993
        }
3✔
994
        current = Cow::Owned(normalized.to_string());
3✔
995
    }
996

997
    current.into_owned()
45✔
998
}
45✔
999

1000
pub fn rewrite_assertion_expression_fixed_point_if_changed(expr: &str) -> Option<String> {
52✔
1001
    let trimmed = expr.trim();
52✔
1002
    if trimmed.is_empty() || !likely_needs_assertion_rewrite(trimmed) {
52✔
1003
        None
9✔
1004
    } else {
1005
        let rewritten = rewrite_assertion_expression_fixed_point(trimmed);
43✔
1006
        if rewritten == trimmed {
43✔
1007
            None
42✔
1008
        } else {
1009
            Some(rewritten)
1✔
1010
        }
1011
    }
1012
}
52✔
1013

1014
pub fn collect_assertion_optimizations(doc: &parser::GctfDocument) -> Vec<OptimizationHint> {
107✔
1015
    let signatures = plugin_signatures();
107✔
1016
    let bool_plugins = boolean_plugins();
107✔
1017
    let mut hints = Vec::new();
107✔
1018

1019
    for section in &doc.sections {
316✔
1020
        if section.section_type != parser::ast::SectionType::Asserts {
316✔
1021
            continue;
271✔
1022
        }
45✔
1023

1024
        for (idx, line) in section.raw_content.lines().enumerate() {
49✔
1025
            let Some(trimmed) = strip_assertion_comments(line) else {
49✔
1026
                continue;
1✔
1027
            };
1028

1029
            if !likely_needs_assertion_rewrite(&trimmed) {
48✔
1030
                continue;
×
1031
            }
48✔
1032

1033
            if let Some((rule_id, rewrite)) =
31✔
1034
                rewrite_assertion_expression_with_context(&trimmed, signatures, bool_plugins)
48✔
1035
            {
1036
                debug_assert!(rule_metadata(rule_id).is_some());
31✔
1037
                hints.push(build_hint(
31✔
1038
                    rule_id,
31✔
1039
                    section_content_line(section.start_line, idx),
31✔
1040
                    &trimmed,
31✔
1041
                    rewrite,
31✔
1042
                ));
1043
            }
17✔
1044
        }
1045
    }
1046

1047
    hints
107✔
1048
}
107✔
1049

1050
#[cfg(test)]
1051
mod tests {
1052
    use super::*;
1053

1054
    #[test]
1055
    fn test_collect_assertion_optimizations_detects_boolean_rewrite() {
1✔
1056
        let content = r#"--- ENDPOINT ---
1✔
1057
test.Service/Method
1✔
1058

1✔
1059
--- ASSERTS ---
1✔
1060
@has_header("x-request-id") == true
1✔
1061
"#;
1✔
1062

1063
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1064
        let hints = collect_assertion_optimizations(&doc);
1✔
1065
        assert_eq!(hints.len(), 1);
1✔
1066
        assert_eq!(hints[0].rule_id, "OPT_B001");
1✔
1067
        assert_eq!(hints[0].after, "@has_header(\"x-request-id\")");
1✔
1068
    }
1✔
1069

1070
    #[test]
1071
    fn test_collect_assertion_optimizations_detects_double_negation_rewrite() {
1✔
1072
        let content = r#"--- ENDPOINT ---
1✔
1073
test.Service/Method
1✔
1074

1✔
1075
--- ASSERTS ---
1✔
1076
!!@has_header("x-request-id")
1✔
1077
"#;
1✔
1078

1079
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1080
        let hints = collect_assertion_optimizations(&doc);
1✔
1081
        assert_eq!(hints.len(), 1);
1✔
1082
        assert_eq!(hints[0].rule_id, "OPT_B005");
1✔
1083
        assert_eq!(hints[0].after, "@has_header(\"x-request-id\")");
1✔
1084
    }
1✔
1085

1086
    #[test]
1087
    fn test_collect_assertion_optimizations_detects_operator_canonicalization() {
1✔
1088
        let content = r#"--- ENDPOINT ---
1✔
1089
test.Service/Method
1✔
1090

1✔
1091
--- ASSERTS ---
1✔
1092
.name startswith "abc"
1✔
1093
"#;
1✔
1094

1095
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1096
        let hints = collect_assertion_optimizations(&doc);
1✔
1097
        assert_eq!(hints.len(), 1);
1✔
1098
        assert_eq!(hints[0].rule_id, "OPT_N001");
1✔
1099
        assert_eq!(hints[0].after, ".name startsWith \"abc\"");
1✔
1100
    }
1✔
1101

1102
    #[test]
1103
    fn test_collect_assertion_optimizations_no_double_negation_for_non_boolean_plugin() {
1✔
1104
        let content = r#"--- ENDPOINT ---
1✔
1105
test.Service/Method
1✔
1106

1✔
1107
--- ASSERTS ---
1✔
1108
!!@len(.items)
1✔
1109
"#;
1✔
1110

1111
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1112
        let hints = collect_assertion_optimizations(&doc);
1✔
1113
        assert!(hints.is_empty());
1✔
1114
    }
1✔
1115

1116
    #[test]
1117
    fn test_collect_assertion_optimizations_constant_fold_numeric_compare() {
1✔
1118
        let content = r#"--- ENDPOINT ---
1✔
1119
test.Service/Method
1✔
1120

1✔
1121
--- ASSERTS ---
1✔
1122
1 + 1 == 2
1✔
1123
3 > 2
1✔
1124
"#;
1✔
1125

1126
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1127
        let hints = collect_assertion_optimizations(&doc);
1✔
1128

1129
        // Only '3 > 2' is a strict literal compare and safe to fold here.
1130
        assert_eq!(hints.len(), 1);
1✔
1131
        assert_eq!(hints[0].rule_id, "OPT_B006");
1✔
1132
        assert_eq!(hints[0].before, "3 > 2");
1✔
1133
        assert_eq!(hints[0].after, "true");
1✔
1134
    }
1✔
1135

1136
    #[test]
1137
    fn test_collect_assertion_optimizations_constant_fold_string_equality() {
1✔
1138
        let content = r#"--- ENDPOINT ---
1✔
1139
test.Service/Method
1✔
1140

1✔
1141
--- ASSERTS ---
1✔
1142
"a" == "a"
1✔
1143
"#;
1✔
1144

1145
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1146
        let hints = collect_assertion_optimizations(&doc);
1✔
1147
        assert_eq!(hints.len(), 1);
1✔
1148
        assert_eq!(hints[0].rule_id, "OPT_B006");
1✔
1149
        assert_eq!(hints[0].after, "true");
1✔
1150
    }
1✔
1151

1152
    #[test]
1153
    fn test_rewrite_rule_metadata_is_complete() {
1✔
1154
        let expected = [
1✔
1155
            "OPT_B001", "OPT_B002", "OPT_B003", "OPT_B004", "OPT_B005", "OPT_B006", "OPT_B007",
1✔
1156
            "OPT_B008", "OPT_B009", "OPT_B010", "OPT_B013", "OPT_B014", "OPT_B015", "OPT_B016",
1✔
1157
            "OPT_B017", "OPT_N001", "OPT_N002", "OPT_I001", "OPT_I002", "OPT_I003", "OPT_I004",
1✔
1158
            "OPT_I005", "OPT_P001", "OPT_P002", "OPT_T001",
1✔
1159
        ];
1✔
1160

1161
        for id in expected {
25✔
1162
            let meta = rule_metadata(id).unwrap_or_else(|| panic!("missing metadata for {id}"));
25✔
1163
            assert!(!meta.preconditions.is_empty());
25✔
1164
            assert!(!meta.negative_cases.is_empty());
25✔
1165
            assert!(!meta.proof_note.is_empty());
25✔
1166
        }
1167
    }
1✔
1168

1169
    #[test]
1170
    fn test_optimization_hint_contains_rule_metadata() {
1✔
1171
        let content = r#"--- ENDPOINT ---
1✔
1172
test.Service/Method
1✔
1173

1✔
1174
--- ASSERTS ---
1✔
1175
@has_header("x") == true
1✔
1176
"#;
1✔
1177

1178
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1179
        let hints = collect_assertion_optimizations(&doc);
1✔
1180
        assert_eq!(hints.len(), 1);
1✔
1181
        assert!(hints[0].preconditions.as_deref().is_some());
1✔
1182
        assert!(hints[0].negative_cases.as_deref().is_some());
1✔
1183
        assert!(hints[0].proof_note.as_deref().is_some());
1✔
1184
    }
1✔
1185

1186
    #[test]
1187
    fn test_collect_assertion_optimizations_reflexive_idempotent_path() {
1✔
1188
        let content = r#"--- ENDPOINT ---
1✔
1189
test.Service/Method
1✔
1190

1✔
1191
--- ASSERTS ---
1✔
1192
.user.id == .user.id
1✔
1193
"#;
1✔
1194

1195
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1196
        let hints = collect_assertion_optimizations(&doc);
1✔
1197

1198
        assert_eq!(hints.len(), 1);
1✔
1199
        assert_eq!(hints[0].rule_id, "OPT_B007");
1✔
1200
        assert_eq!(hints[0].after, "true");
1✔
1201
    }
1✔
1202

1203
    #[test]
1204
    fn test_collect_assertion_optimizations_no_reflexive_for_non_idempotent_plugin() {
1✔
1205
        let content = r#"--- ENDPOINT ---
1✔
1206
test.Service/Method
1✔
1207

1✔
1208
--- ASSERTS ---
1✔
1209
@env("HOME") == @env("HOME")
1✔
1210
"#;
1✔
1211

1212
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1213
        let hints = collect_assertion_optimizations(&doc);
1✔
1214

1215
        assert!(hints.is_empty());
1✔
1216
    }
1✔
1217

1218
    #[test]
1219
    fn test_collect_assertion_optimizations_reflexive_idempotent_inequality() {
1✔
1220
        let content = r#"--- ENDPOINT ---
1✔
1221
test.Service/Method
1✔
1222

1✔
1223
--- ASSERTS ---
1✔
1224
{{ user_id }} != {{ user_id }}
1✔
1225
"#;
1✔
1226

1227
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1228
        let hints = collect_assertion_optimizations(&doc);
1✔
1229

1230
        assert_eq!(hints.len(), 1);
1✔
1231
        assert_eq!(hints[0].rule_id, "OPT_B008");
1✔
1232
        assert_eq!(hints[0].after, "false");
1✔
1233
    }
1✔
1234

1235
    #[test]
1236
    fn test_rewrite_assertion_expression_fixed_point() {
1✔
1237
        let expr = "true == @has_header(\"x-request-id\")";
1✔
1238
        let rewritten = rewrite_assertion_expression_fixed_point(expr);
1✔
1239
        assert_eq!(rewritten, "@has_header(\"x-request-id\")");
1✔
1240
    }
1✔
1241

1242
    #[test]
1243
    fn test_rewrite_assertion_expression_fixed_point_if_changed() {
1✔
1244
        assert_eq!(
1✔
1245
            rewrite_assertion_expression_fixed_point_if_changed(
1✔
1246
                "true == @has_header(\"x-request-id\")"
1✔
1247
            ),
1248
            Some("@has_header(\"x-request-id\")".to_string())
1✔
1249
        );
1250
        assert_eq!(
1✔
1251
            rewrite_assertion_expression_fixed_point_if_changed(".status == 200"),
1✔
1252
            None
1253
        );
1254
    }
1✔
1255

1256
    #[test]
1257
    fn test_collect_assertion_optimizations_ignores_inline_comments() {
1✔
1258
        let content = r#"--- ENDPOINT ---
1✔
1259
test.Service/Method
1✔
1260

1✔
1261
--- ASSERTS ---
1✔
1262
true == @has_header("x-request-id") // comment should be ignored
1✔
1263
"#;
1✔
1264

1265
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1266
        let hints = collect_assertion_optimizations(&doc);
1✔
1267
        assert_eq!(hints.len(), 1);
1✔
1268
        assert_eq!(hints[0].rule_id, "OPT_B003");
1✔
1269
        assert_eq!(hints[0].after, "@has_header(\"x-request-id\")");
1✔
1270
    }
1✔
1271

1272
    #[test]
1273
    fn test_likely_needs_assertion_rewrite_fast_path() {
1✔
1274
        assert!(!likely_needs_assertion_rewrite("@scope_message_count()"));
1✔
1275
        assert!(likely_needs_assertion_rewrite("@elapsed_ms() >= 10"));
1✔
1276
        assert!(likely_needs_assertion_rewrite("true == @has_header(\"x\")"));
1✔
1277
        assert!(likely_needs_assertion_rewrite(".name startswith \"abc\""));
1✔
1278
        assert!(likely_needs_assertion_rewrite("if true then 1 else 2 end"));
1✔
1279
    }
1✔
1280

1281
    // === If-then-else optimization tests ===
1282

1283
    #[test]
1284
    fn test_dead_branch_elimination_true() {
1✔
1285
        let (rule_id, rewritten) =
1✔
1286
            suggest_dead_branch_elimination("if true then \"yes\" else \"no\" end").unwrap();
1✔
1287
        assert_eq!(rule_id, "OPT_I001");
1✔
1288
        assert_eq!(rewritten, "\"yes\"");
1✔
1289
    }
1✔
1290

1291
    #[test]
1292
    fn test_dead_branch_elimination_false() {
1✔
1293
        let (rule_id, rewritten) =
1✔
1294
            suggest_dead_branch_elimination("if false then \"yes\" else \"no\" end").unwrap();
1✔
1295
        assert_eq!(rule_id, "OPT_I001");
1✔
1296
        assert_eq!(rewritten, "\"no\"");
1✔
1297
    }
1✔
1298

1299
    #[test]
1300
    fn test_branch_merging() {
1✔
1301
        let (rule_id, rewritten) =
1✔
1302
            suggest_branch_merging("if .x > 0 then \"same\" else \"same\" end").unwrap();
1✔
1303
        assert_eq!(rule_id, "OPT_I002");
1✔
1304
        assert_eq!(rewritten, "\"same\"");
1✔
1305
    }
1✔
1306

1307
    #[test]
1308
    fn test_nested_if_simplification() {
1✔
1309
        // Pattern: if A then (if A then X else Y end) else Z end
1310
        // Simplified: if A then X else Z end
1311
        let input =
1✔
1312
            "if .a > 0 then (if .a > 0 then \"inner\" else \"other\" end) else \"outer\" end";
1✔
1313
        let result = suggest_nested_if_simplification(input);
1✔
1314
        assert!(result.is_some());
1✔
1315
        let (rule_id, rewritten) = result.unwrap();
1✔
1316
        assert_eq!(rule_id, "OPT_I003");
1✔
1317
        assert_eq!(rewritten, "if .a > 0 then \"inner\" else \"outer\" end");
1✔
1318
    }
1✔
1319

1320
    #[test]
1321
    fn test_parse_if_then_else_simple() {
1✔
1322
        let (cond, then_expr, else_expr) =
1✔
1323
            parse_if_then_else("if .x > 0 then \"yes\" else \"no\" end").unwrap();
1✔
1324
        assert_eq!(cond, ".x > 0");
1✔
1325
        assert_eq!(then_expr, "\"yes\"");
1✔
1326
        assert_eq!(else_expr, "\"no\"");
1✔
1327
    }
1✔
1328

1329
    #[test]
1330
    fn test_parse_if_then_else_nested() {
1✔
1331
        let (cond, then_expr, else_expr) = parse_if_then_else(
1✔
1332
            "if .a > 0 then (if .b > 0 then \"both\" else \"a only\" end) else \"none\" end",
1✔
1333
        )
1✔
1334
        .unwrap();
1✔
1335
        assert_eq!(cond, ".a > 0");
1✔
1336
        assert_eq!(then_expr, "(if .b > 0 then \"both\" else \"a only\" end)");
1✔
1337
        assert_eq!(else_expr, "\"none\"");
1✔
1338
    }
1✔
1339

1340
    #[test]
1341
    fn test_collect_optimizations_detects_dead_branch() {
1✔
1342
        let content = r#"--- ENDPOINT ---
1✔
1343
test.Service/Method
1✔
1344

1✔
1345
--- ASSERTS ---
1✔
1346
if true then "always" else "never" end
1✔
1347
"#;
1✔
1348

1349
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1350
        let hints = collect_assertion_optimizations(&doc);
1✔
1351
        assert_eq!(hints.len(), 1);
1✔
1352
        assert_eq!(hints[0].rule_id, "OPT_I001");
1✔
1353
        assert_eq!(hints[0].after, "\"always\"");
1✔
1354
    }
1✔
1355

1356
    #[test]
1357
    fn test_collect_optimizations_detects_branch_merging() {
1✔
1358
        let content = r#"--- ENDPOINT ---
1✔
1359
test.Service/Method
1✔
1360

1✔
1361
--- ASSERTS ---
1✔
1362
if .x > 0 then "same" else "same" end
1✔
1363
"#;
1✔
1364

1365
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1366
        let hints = collect_assertion_optimizations(&doc);
1✔
1367
        assert_eq!(hints.len(), 1);
1✔
1368
        assert_eq!(hints[0].rule_id, "OPT_I002");
1✔
1369
        assert_eq!(hints[0].after, "\"same\"");
1✔
1370
    }
1✔
1371

1372
    #[test]
1373
    fn test_boolean_simplification() {
1✔
1374
        let (rule_id, rewritten) =
1✔
1375
            suggest_boolean_simplification("if .x > 0 then true else false end").unwrap();
1✔
1376
        assert_eq!(rule_id, "OPT_I004");
1✔
1377
        assert_eq!(rewritten, ".x > 0");
1✔
1378
    }
1✔
1379

1380
    #[test]
1381
    fn test_condition_inversion() {
1✔
1382
        let (rule_id, rewritten) =
1✔
1383
            suggest_condition_inversion("if .x > 0 then false else true end").unwrap();
1✔
1384
        assert_eq!(rule_id, "OPT_I005");
1✔
1385
        assert_eq!(rewritten, "!.x > 0");
1✔
1386
    }
1✔
1387

1388
    #[test]
1389
    fn test_collect_optimizations_boolean_simplification() {
1✔
1390
        let content = r#"--- ENDPOINT ---
1✔
1391
test.Service/Method
1✔
1392

1✔
1393
--- ASSERTS ---
1✔
1394
if @has_header("x") then true else false end
1✔
1395
"#;
1✔
1396

1397
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1398
        let hints = collect_assertion_optimizations(&doc);
1✔
1399
        assert_eq!(hints.len(), 1);
1✔
1400
        assert_eq!(hints[0].rule_id, "OPT_I004");
1✔
1401
        assert_eq!(hints[0].after, "@has_header(\"x\")");
1✔
1402
    }
1✔
1403

1404
    #[test]
1405
    fn test_collect_optimizations_condition_inversion() {
1✔
1406
        let content = r#"--- ENDPOINT ---
1✔
1407
test.Service/Method
1✔
1408

1✔
1409
--- ASSERTS ---
1✔
1410
if .status == 200 then false else true end
1✔
1411
"#;
1✔
1412

1413
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1414
        let hints = collect_assertion_optimizations(&doc);
1✔
1415
        assert_eq!(hints.len(), 1);
1✔
1416
        assert_eq!(hints[0].rule_id, "OPT_I005");
1✔
1417
        assert_eq!(hints[0].after, "!.status == 200");
1✔
1418
    }
1✔
1419

1420
    #[test]
1421
    fn test_parse_if_then_else_string_with_else_keyword() {
1✔
1422
        let (cond, then_expr, else_expr) =
1✔
1423
            parse_if_then_else(r#"if true then " else " else "no" end"#).unwrap();
1✔
1424
        assert_eq!(cond, "true");
1✔
1425
        assert_eq!(then_expr, r#"" else ""#);
1✔
1426
        assert_eq!(else_expr, r#""no""#);
1✔
1427
    }
1✔
1428

1429
    #[test]
1430
    fn test_parse_if_then_else_then_in_string_condition() {
1✔
1431
        let (cond, then_expr, else_expr) =
1✔
1432
            parse_if_then_else(r#"if .x == "then" then "yes" else "no" end"#).unwrap();
1✔
1433
        assert_eq!(cond, r#".x == "then""#);
1✔
1434
        assert_eq!(then_expr, r#""yes""#);
1✔
1435
        assert_eq!(else_expr, r#""no""#);
1✔
1436
    }
1✔
1437

1438
    // === New optimization rules tests ===
1439

1440
    #[test]
1441
    fn test_boolean_identity_or() {
1✔
1442
        // A or true = true
1443
        let (rule_id, rewritten) = suggest_boolean_identity_laws(".x or true").unwrap();
1✔
1444
        assert_eq!(rule_id, "OPT_B009");
1✔
1445
        assert_eq!(rewritten, "true");
1✔
1446

1447
        // A or false = A
1448
        let (rule_id, rewritten) = suggest_boolean_identity_laws(".x or false").unwrap();
1✔
1449
        assert_eq!(rule_id, "OPT_B009");
1✔
1450
        assert_eq!(rewritten, ".x");
1✔
1451

1452
        // true or A = true
1453
        let (rule_id, rewritten) = suggest_boolean_identity_laws("true or .x").unwrap();
1✔
1454
        assert_eq!(rule_id, "OPT_B009");
1✔
1455
        assert_eq!(rewritten, "true");
1✔
1456
    }
1✔
1457

1458
    #[test]
1459
    fn test_boolean_absorption_and() {
1✔
1460
        // A and true = A
1461
        let (rule_id, rewritten) = suggest_boolean_identity_laws(".x and true").unwrap();
1✔
1462
        assert_eq!(rule_id, "OPT_B010");
1✔
1463
        assert_eq!(rewritten, ".x");
1✔
1464

1465
        // A and false = false
1466
        let (rule_id, rewritten) = suggest_boolean_identity_laws(".x and false").unwrap();
1✔
1467
        assert_eq!(rule_id, "OPT_B010");
1✔
1468
        assert_eq!(rewritten, "false");
1✔
1469

1470
        // false and A = false
1471
        let (rule_id, rewritten) = suggest_boolean_identity_laws("false and .x").unwrap();
1✔
1472
        assert_eq!(rule_id, "OPT_B010");
1✔
1473
        assert_eq!(rewritten, "false");
1✔
1474
    }
1✔
1475

1476
    #[test]
1477
    fn test_plugin_length_simplification() {
1✔
1478
        // @len(.x) == 0 → @empty(.x)
1479
        let (rule_id, rewritten) =
1✔
1480
            suggest_plugin_length_simplification("@len(.items) == 0").unwrap();
1✔
1481
        assert_eq!(rule_id, "OPT_P001");
1✔
1482
        assert_eq!(rewritten, "@empty(.items)");
1✔
1483

1484
        // @len(.x) != 0 → @len(.x) > 0
1485
        let (rule_id, rewritten) =
1✔
1486
            suggest_plugin_length_simplification("@len(.items) != 0").unwrap();
1✔
1487
        assert_eq!(rule_id, "OPT_P001");
1✔
1488
        assert_eq!(rewritten, "@len(.items) > 0");
1✔
1489

1490
        // @len(.x) > 0 → no simplification
1491
        let result = suggest_plugin_length_simplification("@len(.items) > 0");
1✔
1492
        assert!(result.is_none());
1✔
1493

1494
        // 0 == @len(.x) → @empty(.x)
1495
        let (rule_id, rewritten) =
1✔
1496
            suggest_plugin_length_simplification("0 == @len(.items)").unwrap();
1✔
1497
        assert_eq!(rule_id, "OPT_P001");
1✔
1498
        assert_eq!(rewritten, "@empty(.items)");
1✔
1499
    }
1✔
1500

1501
    #[test]
1502
    fn test_comparison_negation() {
1✔
1503
        // not (.x == 5) → .x != 5
1504
        let (rule_id, rewritten) = suggest_comparison_negation("not (.x == 5)").unwrap();
1✔
1505
        assert_eq!(rule_id, "OPT_N002");
1✔
1506
        assert_eq!(rewritten, ".x != 5");
1✔
1507

1508
        // not (.x != 5) → .x == 5
1509
        let (rule_id, rewritten) = suggest_comparison_negation("not (.x != 5)").unwrap();
1✔
1510
        assert_eq!(rule_id, "OPT_N002");
1✔
1511
        assert_eq!(rewritten, ".x == 5");
1✔
1512

1513
        // not (.x > 5) → .x <= 5
1514
        let (rule_id, rewritten) = suggest_comparison_negation("not (.x > 5)").unwrap();
1✔
1515
        assert_eq!(rule_id, "OPT_N002");
1✔
1516
        assert_eq!(rewritten, ".x <= 5");
1✔
1517

1518
        // not (.x >= 5) → .x < 5
1519
        let (rule_id, rewritten) = suggest_comparison_negation("not (.x >= 5)").unwrap();
1✔
1520
        assert_eq!(rule_id, "OPT_N002");
1✔
1521
        assert_eq!(rewritten, ".x < 5");
1✔
1522
    }
1✔
1523

1524
    #[test]
1525
    fn test_collect_optimizations_boolean_identity() {
1✔
1526
        let content = r#"--- ENDPOINT ---
1✔
1527
test.Service/Method
1✔
1528

1✔
1529
--- ASSERTS ---
1✔
1530
@has_header("x") or true
1✔
1531
"#;
1✔
1532

1533
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1534
        let hints = collect_assertion_optimizations(&doc);
1✔
1535
        assert_eq!(hints.len(), 1);
1✔
1536
        assert_eq!(hints[0].rule_id, "OPT_B009");
1✔
1537
        assert_eq!(hints[0].after, "true");
1✔
1538
    }
1✔
1539

1540
    #[test]
1541
    fn test_collect_optimizations_plugin_length() {
1✔
1542
        let content = r#"--- ENDPOINT ---
1✔
1543
test.Service/Method
1✔
1544

1✔
1545
--- ASSERTS ---
1✔
1546
@len(.items) == 0
1✔
1547
"#;
1✔
1548

1549
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1550
        let hints = collect_assertion_optimizations(&doc);
1✔
1551
        assert_eq!(hints.len(), 1);
1✔
1552
        assert_eq!(hints[0].rule_id, "OPT_P001");
1✔
1553
        assert_eq!(hints[0].after, "@empty(.items)");
1✔
1554
    }
1✔
1555

1556
    #[test]
1557
    fn test_collect_optimizations_type_aware_uint_gte_zero() {
1✔
1558
        let content = r#"--- ENDPOINT ---
1✔
1559
test.Service/Method
1✔
1560

1✔
1561
--- ASSERTS ---
1✔
1562
@len(.items) >= 0
1✔
1563
"#;
1✔
1564

1565
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1566
        let hints = collect_assertion_optimizations(&doc);
1✔
1567
        assert_eq!(hints.len(), 1);
1✔
1568
        assert_eq!(hints[0].rule_id, "OPT_T001");
1✔
1569
        assert_eq!(hints[0].after, "true");
1✔
1570
    }
1✔
1571

1572
    #[test]
1573
    fn test_collect_optimizations_comparison_negation() {
1✔
1574
        let content = r#"--- ENDPOINT ---
1✔
1575
test.Service/Method
1✔
1576

1✔
1577
--- ASSERTS ---
1✔
1578
not (.status == 200)
1✔
1579
"#;
1✔
1580

1581
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1582
        let hints = collect_assertion_optimizations(&doc);
1✔
1583
        assert_eq!(hints.len(), 1);
1✔
1584
        assert_eq!(hints[0].rule_id, "OPT_N002");
1✔
1585
        assert_eq!(hints[0].after, ".status != 200");
1✔
1586
    }
1✔
1587

1588
    #[test]
1589
    fn test_collect_optimizations_b002_expr_equals_false() {
1✔
1590
        let content = r#"--- ENDPOINT ---
1✔
1591
test.Service/Method
1✔
1592

1✔
1593
--- ASSERTS ---
1✔
1594
@has_header("x") == false
1✔
1595
"#;
1✔
1596
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1597
        let hints = collect_assertion_optimizations(&doc);
1✔
1598
        assert_eq!(hints.len(), 1);
1✔
1599
        assert_eq!(hints[0].rule_id, "OPT_B002");
1✔
1600
        assert_eq!(hints[0].after, "!@has_header(\"x\")");
1✔
1601
    }
1✔
1602

1603
    #[test]
1604
    fn test_collect_optimizations_b004_false_equals_expr() {
1✔
1605
        let content = r#"--- ENDPOINT ---
1✔
1606
test.Service/Method
1✔
1607

1✔
1608
--- ASSERTS ---
1✔
1609
false == @has_header("x")
1✔
1610
"#;
1✔
1611
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1612
        let hints = collect_assertion_optimizations(&doc);
1✔
1613
        assert_eq!(hints.len(), 1);
1✔
1614
        assert_eq!(hints[0].rule_id, "OPT_B004");
1✔
1615
        assert_eq!(hints[0].after, "!@has_header(\"x\")");
1✔
1616
    }
1✔
1617

1618
    #[test]
1619
    fn test_collect_optimizations_b013_inequality_true() {
1✔
1620
        let content = r#"--- ENDPOINT ---
1✔
1621
test.Service/Method
1✔
1622

1✔
1623
--- ASSERTS ---
1✔
1624
@has_header("x") != true
1✔
1625
"#;
1✔
1626
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1627
        let hints = collect_assertion_optimizations(&doc);
1✔
1628
        assert_eq!(hints.len(), 1);
1✔
1629
        assert_eq!(hints[0].rule_id, "OPT_B013");
1✔
1630
        assert_eq!(hints[0].after, "!@has_header(\"x\")");
1✔
1631
    }
1✔
1632

1633
    #[test]
1634
    fn test_collect_optimizations_b014_inequality_false() {
1✔
1635
        let content = r#"--- ENDPOINT ---
1✔
1636
test.Service/Method
1✔
1637

1✔
1638
--- ASSERTS ---
1✔
1639
@has_header("x") != false
1✔
1640
"#;
1✔
1641
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1642
        let hints = collect_assertion_optimizations(&doc);
1✔
1643
        assert_eq!(hints.len(), 1);
1✔
1644
        assert_eq!(hints[0].rule_id, "OPT_B014");
1✔
1645
        assert_eq!(hints[0].after, "@has_header(\"x\")");
1✔
1646
    }
1✔
1647

1648
    #[test]
1649
    fn test_collect_optimizations_b015_true_inequality() {
1✔
1650
        let content = r#"--- ENDPOINT ---
1✔
1651
test.Service/Method
1✔
1652

1✔
1653
--- ASSERTS ---
1✔
1654
true != @has_header("x")
1✔
1655
"#;
1✔
1656
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1657
        let hints = collect_assertion_optimizations(&doc);
1✔
1658
        assert_eq!(hints.len(), 1);
1✔
1659
        assert_eq!(hints[0].rule_id, "OPT_B015");
1✔
1660
        assert_eq!(hints[0].after, "!@has_header(\"x\")");
1✔
1661
    }
1✔
1662

1663
    #[test]
1664
    fn test_collect_optimizations_b016_false_inequality() {
1✔
1665
        let content = r#"--- ENDPOINT ---
1✔
1666
test.Service/Method
1✔
1667

1✔
1668
--- ASSERTS ---
1✔
1669
false != @has_header("x")
1✔
1670
"#;
1✔
1671
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1672
        let hints = collect_assertion_optimizations(&doc);
1✔
1673
        assert_eq!(hints.len(), 1);
1✔
1674
        assert_eq!(hints[0].rule_id, "OPT_B016");
1✔
1675
        assert_eq!(hints[0].after, "@has_header(\"x\")");
1✔
1676
    }
1✔
1677

1678
    #[test]
1679
    fn test_collect_optimizations_b017_double_not_word() {
1✔
1680
        let content = r#"--- ENDPOINT ---
1✔
1681
test.Service/Method
1✔
1682

1✔
1683
--- ASSERTS ---
1✔
1684
not not @has_header("x")
1✔
1685
"#;
1✔
1686
        let doc = parser::parse_gctf_from_str(content, "test.gctf").unwrap();
1✔
1687
        let hints = collect_assertion_optimizations(&doc);
1✔
1688
        assert_eq!(hints.len(), 1);
1✔
1689
        assert_eq!(hints[0].rule_id, "OPT_B017");
1✔
1690
        assert_eq!(hints[0].after, "@has_header(\"x\")");
1✔
1691
    }
1✔
1692

1693
    #[test]
1694
    fn test_collect_optimizations_p002_redundant_parens() {
1✔
1695
        let result = rewrite_assertion_expression_fixed_point("(@has_header(\"x\"))");
1✔
1696
        assert_eq!(result, "@has_header(\"x\")");
1✔
1697
    }
1✔
1698

1699
    #[test]
1700
    fn test_boolean_plugins_contains_uuid() {
1✔
1701
        let bp = boolean_plugins();
1✔
1702
        assert!(bp.contains("uuid"));
1✔
1703
        assert!(bp.contains("email"));
1✔
1704
        assert!(bp.contains("empty"));
1✔
1705
    }
1✔
1706

1707
    #[test]
1708
    fn test_plugin_signatures_returns_map() {
1✔
1709
        let sigs = plugin_signatures();
1✔
1710
        assert!(!sigs.is_empty());
1✔
1711
        assert!(sigs.contains_key("uuid"));
1✔
1712
    }
1✔
1713

1714
    #[test]
1715
    fn test_is_boolean_plugin_expr() {
1✔
1716
        let bp = boolean_plugins();
1✔
1717
        assert!(is_boolean_plugin_expr("@uuid(.x)", bp));
1✔
1718
        assert!(is_boolean_plugin_expr("@empty(.items)", bp));
1✔
1719
        assert!(!is_boolean_plugin_expr("@len(.x)", bp));
1✔
1720
    }
1✔
1721

1722
    #[test]
1723
    fn test_suggest_constant_folding_string_equality() {
1✔
1724
        let result = suggest_constant_folding("\"foo\" == \"foo\"");
1✔
1725
        assert!(result.is_some());
1✔
1726
        let (rule_id, after) = result.unwrap();
1✔
1727
        assert_eq!(rule_id, "OPT_B006");
1✔
1728
        assert_eq!(after, "true");
1✔
1729
    }
1✔
1730

1731
    #[test]
1732
    fn test_suggest_constant_folding_mixed_types() {
1✔
1733
        let result = suggest_constant_folding("\"foo\" == 123");
1✔
1734
        assert!(result.is_some());
1✔
1735
        let (_rule_id, after) = result.unwrap();
1✔
1736
        assert_eq!(after, "false");
1✔
1737
    }
1✔
1738

1739
    #[test]
1740
    fn test_suggest_constant_folding_invalid_json() {
1✔
1741
        let result = suggest_constant_folding("@len(.x) == 5");
1✔
1742
        assert!(result.is_none());
1✔
1743
    }
1✔
1744
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc