Skip to main content

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

1use ra_ap_syntax::{AstNode, ast, syntax_editor::SyntaxEditor};
2
3use crate::{AstCtx, Example, Violation};
4
5#[rustfmt::skip]
6const EXAMPLES: &[Example] = &[
7    Example {
8        label: "println in library",
9        code: "fn f() { println!(\"hello\"); }",
10        pass: false,
11    },
12    Example {
13        label: "eprintln in library",
14        code: "fn f() { eprintln!(\"error\"); }",
15        pass: false,
16    },
17    Example {
18        label: "print in library",
19        code: "fn f() { print!(\"hello\"); }",
20        pass: false,
21    },
22    Example {
23        label: "eprint in library",
24        code: "fn f() { eprint!(\"error\"); }",
25        pass: false,
26    },
27    Example {
28        label: "println in test module",
29        code: "#[cfg(test)]\nmod tests {\n    fn t() { println!(\"debug\"); }\n}",
30        pass: true,
31    },
32    Example {
33        label: "no println",
34        code: "fn f() { let x = 1; }",
35        pass: true,
36    },
37    Example {
38        label: "println in string literal",
39        code: r#"fn f() { let s = "println!(value)"; }"#,
40        pass: true,
41    },
42];
43
44crate::ast_tree_rule!(
45    println,
46    "Ban `println!`/`eprintln!`/`print!`/`eprint!` in library code.",
47    "Console printing bypasses structured logging. Use tracing or the output module for consistent, filterable output.",
48    Medium,
49    fix_println,
50);
51
52const BANNED_MACROS: &[&str] = &["println", "eprintln", "print", "eprint"];
53
54fn check_println(ctx: &AstCtx<'_>) -> Vec<Violation> {
55    ctx.nodes::<ast::MacroCall>()
56        .filter(|call| !ctx.is_in_test(call))
57        .filter_map(|call| {
58            let name = unqualified_macro_name(&call)?;
59
60            BANNED_MACROS.contains(&name.as_str()).then(|| {
61                ctx.violation(
62                    &call,
63                    format!("{name}!() in library code (use tracing or return errors)"),
64                )
65            })
66        })
67        .collect()
68}
69
70fn unqualified_macro_name(call: &ast::MacroCall) -> Option<String> {
71    let path = call.path()?;
72
73    if path.qualifier().is_some() {
74        return None;
75    }
76
77    path.segment()?
78        .name_ref()
79        .map(|name| name.text().to_string())
80}
81
82// #t(fn: rust_clone_in_loop) SyntaxEditor requires an owned deletion target per macro call
83fn fix_println(ctx: &AstCtx<'_>, _violations: &[Violation]) -> Option<String> {
84    let (editor, root) = SyntaxEditor::with_ast_node(ctx.root);
85    let mut changed = false;
86
87    for call in root
88        .syntax()
89        .descendants()
90        .filter_map(ast::MacroCall::cast)
91        .filter(|call| !ctx.is_in_test(call))
92        .filter(|call| {
93            unqualified_macro_name(call).is_some_and(|name| BANNED_MACROS.contains(&name.as_str()))
94        })
95    {
96        let target = call
97            .syntax()
98            .parent()
99            .and_then(ast::ExprStmt::cast)
100            .map_or_else(
101                || call.syntax().clone(),
102                |statement| statement.syntax().clone(),
103            );
104
105        editor.delete(target);
106        changed = true;
107    }
108
109    changed.then(|| editor.finish().new_root().to_string())
110}
111
112crate::tidy_ast_test!(check_println, {
113    crate::example_tests!(EXAMPLES, check_println);
114    crate::fix_tests!(ast_tree, check_println, fix_println);
115});