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 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 fn fix(&self, _finding: &RuleFinding, _source_text: &str) -> Option<Fix> {
73 None
74 }
75
76 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 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 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 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 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 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 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 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}