Skip to main content

react_auditor/rules/quality/
no_empty_blocks.rs

1use oxc_ast::ast::{Program, Statement};
2use oxc_ast_visit::Visit;
3use oxc_ast_visit::walk::walk_statement;
4use oxc_semantic::Semantic;
5
6use crate::rules::{Fix, Rule, RuleFinding, RuleMeta, Severity};
7
8pub struct NoEmptyBlocks;
9
10const RULE_META: RuleMeta = RuleMeta {
11    id: "no-empty-blocks",
12    default_severity: Severity::Warning,
13    category: "quality",
14    description: "No empty if/while/for/try/catch/finally blocks",
15};
16
17impl Rule for NoEmptyBlocks {
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 = EmptyBlockCollector {
24            findings: Vec::new(),
25            source: source_text,
26        };
27        collector.visit_program(program);
28        collector.findings
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 close = after.find('}')?;
39        Some(Fix {
40            start,
41            end: start + close + 1,
42            replacement: String::new(),
43        })
44    }
45}
46
47struct EmptyBlockCollector<'a> {
48    findings: Vec<RuleFinding>,
49    source: &'a str,
50}
51
52impl<'a> Visit<'a> for EmptyBlockCollector<'a> {
53    fn visit_statement(&mut self, stmt: &Statement<'a>) {
54        match stmt {
55            Statement::IfStatement(if_stmt) => {
56                if let Some(alt) = &if_stmt.alternate
57                    && let Statement::BlockStatement(block) = alt
58                    && block.body.is_empty()
59                {
60                    let start = block.span.start as usize;
61                    let line = self.source[..start].lines().count().max(1);
62                    let col = start - self.source[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
63                    self.findings.push(RuleFinding {
64                        line,
65                        column: col + 1,
66                        message: "Empty else block".to_string(),
67                    });
68                }
69                if let Statement::BlockStatement(block) = &if_stmt.consequent
70                    && block.body.is_empty()
71                {
72                    let start = block.span.start as usize;
73                    let line = self.source[..start].lines().count().max(1);
74                    let col = start - self.source[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
75                    self.findings.push(RuleFinding {
76                        line,
77                        column: col + 1,
78                        message: "Empty if block".to_string(),
79                    });
80                }
81            }
82            Statement::WhileStatement(while_stmt) => {
83                if let Statement::BlockStatement(block) = &while_stmt.body
84                    && block.body.is_empty()
85                {
86                    let start = block.span.start as usize;
87                    let line = self.source[..start].lines().count().max(1);
88                    let col = start - self.source[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
89                    self.findings.push(RuleFinding {
90                        line,
91                        column: col + 1,
92                        message: "Empty while loop body".to_string(),
93                    });
94                }
95            }
96            Statement::ForStatement(for_stmt) => {
97                if let Statement::BlockStatement(block) = &for_stmt.body
98                    && block.body.is_empty()
99                {
100                    let start = block.span.start as usize;
101                    let line = self.source[..start].lines().count().max(1);
102                    let col = start - self.source[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
103                    self.findings.push(RuleFinding {
104                        line,
105                        column: col + 1,
106                        message: "Empty for loop body".to_string(),
107                    });
108                }
109            }
110            Statement::TryStatement(try_stmt) => {
111                if let Some(handler) = &try_stmt.handler
112                    && handler.body.body.is_empty()
113                {
114                    let start = handler.body.span.start as usize;
115                    let line = self.source[..start].lines().count().max(1);
116                    let col = start - self.source[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
117                    self.findings.push(RuleFinding {
118                        line,
119                        column: col + 1,
120                        message: "Empty catch block".to_string(),
121                    });
122                }
123                if let Some(finalizer) = &try_stmt.finalizer
124                    && finalizer.body.is_empty()
125                {
126                    let start = finalizer.span.start as usize;
127                    let line = self.source[..start].lines().count().max(1);
128                    let col = start - self.source[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
129                    self.findings.push(RuleFinding {
130                        line,
131                        column: col + 1,
132                        message: "Empty finally block".to_string(),
133                    });
134                }
135            }
136            _ => {}
137        }
138
139        walk_statement(self, stmt);
140    }
141}