wowlab_tidy/languages/rust/rules/correctness/
assert_side_effects.rs1use ra_ap_syntax::{
2 AstNode,
3 ast::{self},
4};
5
6use crate::{AstCtx, Example, Violation};
7
8#[rustfmt::skip]
9const EXAMPLES: &[Example] = &[
10 Example {
11 label: "compound assign in debug_assert",
12 code: "fn f() { let mut x = 0; debug_assert!({ x += 1; x > 0 }); }",
13 pass: false,
14 },
15 Example {
16 label: "simple comparison",
17 code: "fn f() { debug_assert!(x > 0); }",
18 pass: true,
19 },
20 Example {
21 label: "debug_assert_eq passes",
22 code: "fn f() { debug_assert_eq!(a, b); }",
23 pass: true,
24 },
25 Example {
26 label: "subtract assign in debug_assert",
27 code: "fn f() { let mut x = 5; debug_assert!({ x -= 1; x > 0 }); }",
28 pass: false,
29 },
30 Example {
31 label: "compound assign in test module",
32 code: "#[cfg(test)]\nmod tests {\n fn t() { let mut x = 0; debug_assert!({ x += 1; x > 0 }); }\n}",
33 pass: true,
34 },
35];
36
37crate::ast_rule!(
38 assert_side_effects,
39 "Ban compound assignments (`+=`, `-=`) inside `debug_assert!` macros.",
40 "Compound assignments inside debug_assert! are side effects that vanish in release builds, causing silent behavior changes.",
41 High,
42);
43
44const DEBUG_ASSERT_MACROS: &[&str] = &["debug_assert", "debug_assert_eq", "debug_assert_ne"];
45const MAX_COMPOUND_OPERATOR_LEN: usize = 3;
46
47fn check_assert_side_effects(ctx: &AstCtx<'_>) -> Vec<Violation> {
48 ctx.nodes::<ast::MacroCall>()
49 .filter(|call| !ctx.is_in_test(call))
50 .filter_map(|call| {
51 let name = macro_name(&call)?;
52
53 if !DEBUG_ASSERT_MACROS.contains(&name.as_str()) {
54 return None;
55 }
56
57 let op = has_compound_assign(&call)?;
58
59 Some(ctx.violation(
60 &call,
61 format!(
62 "{name}!() contains compound assignment `{op}` — side effect lost in release builds"
63 ),
64 ))
65 })
66 .collect()
67}
68
69fn macro_name(call: &ast::MacroCall) -> Option<String> {
70 call.path()?
71 .segment()?
72 .name_ref()
73 .map(|name| name.text().to_string())
74}
75
76fn has_compound_assign(call: &ast::MacroCall) -> Option<&'static str> {
78 const OPERATORS: &[&str] = &["+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>="];
79 let mut punctuation = String::new();
80
81 for token in call
82 .token_tree()?
83 .syntax()
84 .descendants_with_tokens()
85 .filter_map(ra_ap_syntax::NodeOrToken::into_token)
86 .filter(|token| !token.kind().is_trivia())
87 {
88 if !token.kind().is_punct()
89 || matches!(token.text(), "(" | ")" | "[" | "]" | "{" | "}" | "," | ";")
90 {
91 punctuation.clear();
92 continue;
93 }
94
95 punctuation.push_str(token.text());
96
97 if let Some(operator) = OPERATORS
98 .iter()
99 .copied()
100 .find(|operator| punctuation.ends_with(operator))
101 {
102 return Some(operator);
103 }
104
105 if punctuation.len() > MAX_COMPOUND_OPERATOR_LEN {
106 punctuation.remove(0);
107 }
108 }
109
110 None
111}
112
113crate::tidy_ast_test!(check_assert_side_effects, {
114 crate::example_tests!(EXAMPLES, check_assert_side_effects);
115});