react_auditor/rules/
mod.rs1use 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 fn fix(&self, _finding: &RuleFinding, _source_text: &str) -> Option<Fix> {
74 None
75 }
76
77 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 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 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 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 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 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 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 self.rules
261 .push(Box::new(performance::no_large_libraries::NoLargeLibraries));
262 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}