Skip to main content

wowlab_tidy/languages/rust/rules/hygiene/
dbg.rs

1use ra_ap_syntax::{AstNode, ast, syntax_editor::SyntaxEditor};
2
3use super::support::parse_expr;
4use crate::{AstCtx, Example, Violation};
5
6#[rustfmt::skip]
7const EXAMPLES: &[Example] = &[
8    Example {
9        label: "dbg! left in code",
10        code: "fn f() { dbg!(1); }",
11        pass: false,
12    },
13    Example {
14        label: "no dbg",
15        code: "fn f() { let x = 1; }",
16        pass: true,
17    },
18    Example {
19        label: "dbg in string literal",
20        code: r#"fn f() { let s = "dbg!(value)"; }"#,
21        pass: true,
22    },
23    Example {
24        label: "dbg in test module",
25        code: "#[cfg(test)]\nmod tests {\n    fn t() { dbg!(1); }\n}",
26        pass: true,
27    },
28    Example {
29        label: "production dbg with test module",
30        code: "fn prod() { dbg!(1); }\n#[cfg(test)]\nmod tests {\n    fn t() { dbg!(2); }\n}",
31        pass: false,
32    },
33];
34
35crate::ast_tree_rule!(
36    dbg,
37    "Ban `dbg!()` macro calls in production code.",
38    "dbg!() writes to stderr and is meant for temporary debugging. Leaving it in production pollutes output.",
39    Medium,
40    fix_dbg,
41);
42
43fn check_dbg(ctx: &AstCtx<'_>) -> Vec<Violation> {
44    ctx.nodes::<ast::MacroCall>()
45        .filter(|call| !ctx.is_in_test(call) && is_unqualified_macro(call, "dbg"))
46        .map(|call| ctx.violation(&call, "dbg!() call left in code"))
47        .collect()
48}
49
50fn is_unqualified_macro(call: &ast::MacroCall, expected: &str) -> bool {
51    call.path().is_some_and(|path| {
52        path.qualifier().is_none()
53            && path
54                .segment()
55                .and_then(|segment| segment.name_ref())
56                .is_some_and(|name| name.text() == expected)
57    })
58}
59
60// #t(fn: rust_clone_in_loop) SyntaxEditor owns both replacement syntax nodes for each edit
61fn fix_dbg(ctx: &AstCtx<'_>, _violations: &[Violation]) -> Option<String> {
62    let (editor, root) = SyntaxEditor::with_ast_node(ctx.root);
63    let mut changed = false;
64
65    for call in root
66        .syntax()
67        .descendants()
68        .filter_map(ast::MacroCall::cast)
69        .filter(|call| !ctx.is_in_test(call) && is_unqualified_macro(call, "dbg"))
70    {
71        let inner = macro_inner(&call)?;
72        let replacement = parse_expr(&inner)?;
73
74        editor.replace(call.syntax().clone(), replacement.syntax().clone());
75        changed = true;
76    }
77
78    changed.then(|| editor.finish().new_root().to_string())
79}
80
81fn macro_inner(call: &ast::MacroCall) -> Option<String> {
82    let text = call.token_tree()?.syntax().text().to_string();
83    let mut chars = text.chars();
84    let open = chars.next()?;
85    let close = text.chars().last()?;
86
87    if !matches!((open, close), ('(', ')') | ('[', ']') | ('{', '}')) {
88        return None;
89    }
90
91    text.get(open.len_utf8()..text.len().checked_sub(close.len_utf8())?)
92        .map(str::to_owned)
93}
94
95crate::tidy_ast_test!(check_dbg, {
96    crate::example_tests!(EXAMPLES, check_dbg);
97    crate::fix_tests!(ast_tree, check_dbg, fix_dbg);
98});