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