react_auditor/rules/quality/
no_console.rs1use oxc_ast::ast::{CallExpression, Expression, Program};
2use oxc_ast_visit::Visit;
3use oxc_ast_visit::walk::walk_call_expression;
4use oxc_semantic::Semantic;
5
6use crate::rules::{Fix, Rule, RuleFinding, RuleMeta, Severity};
7
8pub struct NoConsole;
9
10const RULE_META: RuleMeta = RuleMeta {
11 id: "no-console",
12 default_severity: Severity::Warning,
13 category: "quality",
14 description: "Avoid console.log in production code",
15};
16
17impl Rule for NoConsole {
18 fn meta(&self) -> &RuleMeta {
19 &RULE_META
20 }
21
22 fn run(&self, program: &Program, _semantic: &Semantic, source_text: &str) -> Vec<RuleFinding> {
23 let mut collector = ConsoleCallCollector {
24 calls: Vec::new(),
25 source: source_text,
26 };
27 collector.visit_program(program);
28 collector.calls
29 }
30
31 fn has_fix(&self) -> bool {
32 true
33 }
34
35 fn fix(&self, finding: &RuleFinding, source_text: &str) -> Option<Fix> {
36 let start = crate::rules::line_col_to_offset(source_text, finding.line, finding.column)?;
37 let after = &source_text[start..];
38 let mut depth = 0;
39 let mut end = 0;
40 for (i, ch) in after.char_indices() {
41 if ch == '(' {
42 depth += 1;
43 } else if ch == ')' {
44 depth -= 1;
45 if depth == 0 {
46 end = start + i + 1;
47 break;
48 }
49 }
50 }
51 if depth != 0 || end == 0 {
52 return None;
53 }
54 let after_fix = &source_text[end..];
55 if after_fix.starts_with(';') {
56 end += 1;
57 }
58 Some(Fix {
59 start,
60 end,
61 replacement: String::new(),
62 })
63 }
64}
65
66struct ConsoleCallCollector<'a> {
67 calls: Vec<RuleFinding>,
68 source: &'a str,
69}
70
71impl<'a> Visit<'a> for ConsoleCallCollector<'a> {
72 fn visit_call_expression(&mut self, expr: &CallExpression<'a>) {
73 if let Expression::StaticMemberExpression(member) = &expr.callee
74 && let Expression::Identifier(ident) = &member.object
75 && ident.name.as_str() == "console"
76 {
77 let start = expr.span.start as usize;
78 let end = expr.span.end as usize;
79
80 let line = self.source[..start].lines().count().max(1);
81 let col = start - self.source[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
82 let snippet = &self.source[start..end.min(self.source.len())];
83
84 self.calls.push(RuleFinding {
85 line,
86 column: col + 1,
87 message: format!("Unexpected console statement: {snippet}"),
88 });
89 }
90
91 walk_call_expression(self, expr);
92 }
93}