Skip to main content

react_auditor/rules/quality/
no_console.rs

1use 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}