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