Skip to main content

react_auditor/rules/
mod.rs

1use std::collections::HashMap;
2
3use oxc_ast::ast::Program;
4use oxc_semantic::Semantic;
5
6pub mod nextjs;
7pub mod performance;
8pub mod quality;
9pub mod react;
10pub mod security;
11pub mod typescript;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum Severity {
15    Error,
16    Warning,
17    Off,
18}
19
20impl std::str::FromStr for Severity {
21    type Err = ();
22
23    fn from_str(s: &str) -> Result<Self, Self::Err> {
24        match s.to_lowercase().as_str() {
25            "error" => Ok(Severity::Error),
26            "warn" | "warning" => Ok(Severity::Warning),
27            _ => Ok(Severity::Off),
28        }
29    }
30}
31
32impl Severity {
33    pub fn is_on(&self) -> bool {
34        !matches!(self, Severity::Off)
35    }
36}
37
38#[derive(Debug, Clone)]
39pub struct RuleMeta {
40    pub id: &'static str,
41    pub default_severity: Severity,
42    pub category: &'static str,
43    pub description: &'static str,
44}
45
46#[derive(Debug, Clone)]
47pub struct Violation {
48    pub file_path: String,
49    pub line: usize,
50    pub column: usize,
51    pub rule_id: String,
52    pub category: String,
53    pub message: String,
54    pub severity: Severity,
55}
56
57impl Violation {
58    pub fn to_finding(&self) -> RuleFinding {
59        RuleFinding {
60            line: self.line,
61            column: self.column,
62            message: self.message.clone(),
63        }
64    }
65}
66
67pub trait Rule: Send + Sync {
68    fn meta(&self) -> &RuleMeta;
69    fn run(&self, program: &Program, semantic: &Semantic, source_text: &str) -> Vec<RuleFinding>;
70    /// If this rule supports auto-fix, return the byte span and replacement text.
71    /// Default implementation returns `None` (no fix available).
72    fn fix(&self, _finding: &RuleFinding, _source_text: &str) -> Option<Fix> {
73        None
74    }
75
76    /// Whether this rule has auto-fix capability.
77    fn has_fix(&self) -> bool {
78        false
79    }
80}
81
82pub struct RuleFinding {
83    pub line: usize,
84    pub column: usize,
85    pub message: String,
86}
87
88pub struct Fix {
89    pub start: usize,
90    pub end: usize,
91    pub replacement: String,
92}
93
94pub struct RuleRegistry {
95    rules: Vec<Box<dyn Rule>>,
96}
97
98impl Default for RuleRegistry {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104impl RuleRegistry {
105    pub fn new() -> Self {
106        let mut registry = Self { rules: Vec::new() };
107        registry.register_all();
108        registry
109    }
110
111    fn register_all(&mut self) {
112        // ── Phase 4: Code Quality ──
113        self.rules.push(Box::new(quality::no_console::NoConsole));
114        self.rules
115            .push(Box::new(quality::no_empty_blocks::NoEmptyBlocks));
116        self.rules.push(Box::new(quality::no_var::NoVar));
117        self.rules.push(Box::new(quality::max_params::MaxParams));
118        self.rules
119            .push(Box::new(quality::no_long_functions::NoLongFunctions));
120        self.rules
121            .push(Box::new(quality::prefer_early_return::PreferEarlyReturn));
122        self.rules
123            .push(Box::new(quality::no_commented_code::NoCommentedCode));
124        self.rules
125            .push(Box::new(quality::no_deep_nesting::NoDeepNesting));
126        self.rules
127            .push(Box::new(quality::no_magic_numbers::NoMagicNumbers));
128        self.rules
129            .push(Box::new(quality::consistent_return::ConsistentReturn));
130        self.rules
131            .push(Box::new(quality::no_unused_vars_rule::NoUnusedVars));
132        self.rules.push(Box::new(quality::no_shadow::NoShadow));
133        self.rules.push(Box::new(quality::complexity::Complexity));
134        // ── Phase 5: React ──
135        self.rules
136            .push(Box::new(react::no_missing_key::NoMissingKey));
137        self.rules
138            .push(Box::new(react::no_inline_styles::NoInlineStyles));
139        self.rules.push(Box::new(
140            react::consistent_component_naming::ConsistentComponentNaming,
141        ));
142        self.rules.push(Box::new(react::no_index_key::NoIndexKey));
143        self.rules
144            .push(Box::new(react::no_inline_functions::NoInlineFunctions));
145        self.rules.push(Box::new(
146            react::prefer_function_components::PreferFunctionComponents,
147        ));
148        self.rules
149            .push(Box::new(react::no_unnecessary_memo::NoUnnecessaryMemo));
150        self.rules.push(Box::new(
151            react::no_multiple_render_methods::NoMultipleRenderMethods,
152        ));
153        self.rules.push(Box::new(
154            react::no_side_effects_in_render::NoSideEffectsInRender,
155        ));
156        self.rules.push(Box::new(react::hook_rules::HookRules));
157        self.rules
158            .push(Box::new(react::effect_deps_complete::EffectDepsComplete));
159        self.rules
160            .push(Box::new(react::no_set_state_in_effect::NoSetStateInEffect));
161        self.rules
162            .push(Box::new(react::no_set_state_in_render::NoSetStateInRender));
163        self.rules
164            .push(Box::new(react::no_duplicate_props::NoDuplicateProps));
165        self.rules
166            .push(Box::new(react::no_direct_mutation::NoDirectMutation));
167        self.rules.push(Box::new(
168            react::no_ref_in_component_name::NoRefInComponentName,
169        ));
170        self.rules
171            .push(Box::new(react::no_forward_ref::NoForwardRef));
172        // ── Phase 6: TypeScript ──
173        self.rules.push(Box::new(typescript::no_any::NoAny));
174        self.rules.push(Box::new(
175            typescript::no_non_null_assertion::NoNonNullAssertion,
176        ));
177        self.rules
178            .push(Box::new(typescript::no_type_assertion::NoTypeAssertion));
179        self.rules
180            .push(Box::new(typescript::no_empty_interface::NoEmptyInterface));
181        self.rules.push(Box::new(
182            typescript::consistent_type_imports::ConsistentTypeImports,
183        ));
184        self.rules.push(Box::new(
185            typescript::explicit_return_type::ExplicitReturnType,
186        ));
187        self.rules
188            .push(Box::new(typescript::strict_null_checks::StrictNullChecks));
189        self.rules
190            .push(Box::new(typescript::prefer_interface::PreferInterface));
191        self.rules
192            .push(Box::new(typescript::no_explicit_any::NoExplicitAny));
193        // ── Phase 7: Security ──
194        self.rules.push(Box::new(
195            security::no_dangerously_set_innerhtml::NoDangerouslySetInnerHtml,
196        ));
197        self.rules.push(Box::new(security::no_eval::NoEval));
198        self.rules
199            .push(Box::new(security::no_script_url::NoScriptUrl));
200        self.rules
201            .push(Box::new(security::no_hardcoded_secrets::NoHardcodedSecrets));
202        self.rules
203            .push(Box::new(security::no_unsanitized_input::NoUnsanitizedInput));
204        self.rules
205            .push(Box::new(security::no_insecure_protocol::NoInsecureProtocol));
206        self.rules
207            .push(Box::new(security::no_unsafe_iframe::NoUnsafeIframe));
208        // ── Phase 8: Performance & Accessibility ──
209        self.rules
210            .push(Box::new(performance::prefer_fragments::PreferFragments));
211        self.rules
212            .push(Box::new(performance::no_bind_in_jsx::NoBindInJsx));
213        self.rules.push(Box::new(performance::img_alt::ImgAlt));
214        self.rules
215            .push(Box::new(performance::button_has_type::ButtonHasType));
216        self.rules
217            .push(Box::new(performance::label_associated::LabelAssociated));
218        self.rules.push(Box::new(
219            performance::no_heavy_computation_in_render::NoHeavyComputationInRender,
220        ));
221        self.rules.push(Box::new(
222            performance::lazy_load_components::LazyLoadComponents,
223        ));
224        self.rules
225            .push(Box::new(performance::aria_valid::AriaValid));
226        self.rules
227            .push(Box::new(performance::heading_levels::HeadingLevels));
228        self.rules
229            .push(Box::new(performance::a_has_content::AHasContent));
230        self.rules.push(Box::new(
231            performance::no_ambiguous_labels::NoAmbiguousLabels,
232        ));
233        self.rules.push(Box::new(
234            performance::tabindex_no_positive::TabindexNoPositive,
235        ));
236        self.rules.push(Box::new(
237            performance::click_events_have_key_events::ClickEventsHaveKeyEvents,
238        ));
239        self.rules
240            .push(Box::new(performance::html_has_lang::HtmlHasLang));
241        self.rules
242            .push(Box::new(performance::no_autofocus::NoAutofocus));
243        // ── Phase 12: Next.js ──
244        self.rules
245            .push(Box::new(nextjs::no_img_element::NoImgElement));
246        self.rules
247            .push(Box::new(nextjs::no_script_tag_in_head::NoScriptTagInHead));
248        self.rules.push(Box::new(nextjs::no_page_link::NoPageLink));
249        self.rules
250            .push(Box::new(nextjs::no_head_element::NoHeadElement));
251        self.rules
252            .push(Box::new(nextjs::no_sync_script::NoSyncScript));
253        // ── Phase 14 continued: Performance ──
254        self.rules
255            .push(Box::new(performance::no_large_libraries::NoLargeLibraries));
256    }
257
258    pub fn run_rules(
259        &self,
260        program: &Program,
261        semantic: &Semantic,
262        source_text: &str,
263        file_path: &str,
264        severity_overrides: &HashMap<String, String>,
265        category_filter: Option<&Vec<String>>,
266    ) -> Vec<Violation> {
267        let mut violations = Vec::new();
268
269        for rule in &self.rules {
270            let meta = rule.meta();
271
272            if let Some(categories) = &category_filter
273                && !categories.contains(&meta.category.to_string())
274            {
275                continue;
276            }
277
278            let effective_severity = severity_overrides
279                .get(meta.id)
280                .map(|s| s.parse::<Severity>().unwrap())
281                .unwrap_or_else(|| meta.default_severity.clone());
282
283            if !effective_severity.is_on() {
284                continue;
285            }
286
287            let findings = rule.run(program, semantic, source_text);
288
289            for finding in &findings {
290                violations.push(Violation {
291                    file_path: file_path.to_string(),
292                    line: finding.line,
293                    column: finding.column,
294                    rule_id: meta.id.to_string(),
295                    category: meta.category.to_string(),
296                    message: finding.message.clone(),
297                    severity: effective_severity.clone(),
298                });
299            }
300        }
301
302        violations
303    }
304
305    pub fn get_rule_ids(&self) -> Vec<&'static str> {
306        self.rules.iter().map(|r| r.meta().id).collect()
307    }
308
309    pub fn get_rule(&self, rule_id: &str) -> Option<&dyn Rule> {
310        self.rules
311            .iter()
312            .find(|r| r.meta().id == rule_id)
313            .map(|v| v.as_ref())
314    }
315}
316
317pub fn line_col_to_offset(source: &str, line: usize, col: usize) -> Option<usize> {
318    let mut current_line = 1;
319    let mut offset = 0;
320    for (i, _) in source.char_indices() {
321        if current_line == line {
322            return Some(offset + col - 1);
323        }
324        if source.as_bytes().get(i) == Some(&b'\n') {
325            current_line += 1;
326            offset = i + 1;
327        }
328    }
329    None
330}