wowlab_tidy/languages/rust/rules/hygiene/
dbg.rs1use 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
60fn 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});