oparry_validators/
performance.rs1use crate::Validator;
4use oparry_core::{Issue, IssueLevel, Result, ValidationResult};
5use oparry_parser::{ParsedCode, Language};
6use regex::Regex;
7use std::path::Path;
8
9#[derive(Debug, Clone)]
11pub struct PerformanceConfig {
12 pub require_memo_for_props: bool,
14 pub check_hook_usage: bool,
16 pub check_lazy_loading: bool,
18 pub warn_inline_creation: bool,
20 pub max_render_complexity: usize,
22}
23
24impl Default for PerformanceConfig {
25 fn default() -> Self {
26 Self {
27 require_memo_for_props: true,
28 check_hook_usage: true,
29 check_lazy_loading: true,
30 warn_inline_creation: true,
31 max_render_complexity: 100,
32 }
33 }
34}
35
36pub struct PerformanceValidator {
38 config: PerformanceConfig,
39 component_regex: Regex,
40 useeffect_regex: Regex,
41}
42
43impl PerformanceValidator {
44 pub fn new(config: PerformanceConfig) -> Self {
46 Self {
47 config,
48 component_regex: Regex::new(r"(?:function|const)\s+(\w+)\s*(?:\(|=|\(\))").unwrap(),
49 useeffect_regex: Regex::new(r"useEffect\s*\(").unwrap(),
50 }
51 }
52
53 pub fn default_config() -> Self {
55 Self::new(PerformanceConfig::default())
56 }
57
58 fn check_useeffect_fetching(&self, source: &str, file: &str) -> Vec<Issue> {
60 let mut issues = Vec::new();
61 if !self.config.check_hook_usage {
62 return issues;
63 }
64
65 let lines: Vec<&str> = source.lines().collect();
66 let mut in_useeffect = false;
67 let mut useEffect_depth = 0;
68
69 for (idx, line) in lines.iter().enumerate() {
70 if self.useeffect_regex.is_match(line) {
71 in_useeffect = true;
72 useEffect_depth = 1;
73 if line.contains("fetch(") || line.contains("axios.") {
75 issues.push(Issue::warning(
76 "perf-useeffect-fetch",
77 "Data fetching in useEffect - prefer React Query/SWR",
78 )
79 .with_file(file)
80 .with_line(idx + 1)
81 .with_suggestion("Use @tanstack/react-query for data fetching"));
82 }
83 } else if in_useeffect {
84 useEffect_depth += line.matches('{').count() as i32;
85 useEffect_depth -= line.matches('}').count() as i32;
86
87 if line.contains("fetch(") || line.contains("axios.") {
88 issues.push(Issue::warning(
89 "perf-useeffect-fetch",
90 "Data fetching in useEffect - prefer React Query/SWR",
91 )
92 .with_file(file)
93 .with_line(idx + 1)
94 .with_suggestion("Use @tanstack/react-query for data fetching"));
95 }
96
97 if useEffect_depth <= 0 {
98 in_useeffect = false;
99 }
100 }
101 }
102 issues
103 }
104
105 fn check_missing_keys(&self, source: &str, file: &str) -> Vec<Issue> {
107 let mut issues = Vec::new();
108 let key_regex = Regex::new(r#"\.map\s*\([^)]*\)\s*=>\s*<"#).unwrap();
109
110 for (idx, line) in source.lines().enumerate() {
111 if key_regex.is_match(line) && !line.contains("key=") {
112 issues.push(Issue::warning(
113 "perf-missing-key",
114 "Missing 'key' prop in list rendering",
115 )
116 .with_file(file)
117 .with_line(idx + 1)
118 .with_suggestion("Add unique key prop for efficient rendering"));
119 }
120 }
121 issues
122 }
123}
124
125impl Validator for PerformanceValidator {
126 fn name(&self) -> &str {
127 "Performance"
128 }
129
130 fn supports(&self, language: Language) -> bool {
131 language.is_javascript_variant()
132 }
133
134 fn validate_parsed(&self, code: &ParsedCode, file: &Path) -> Result<ValidationResult> {
135 let mut result = ValidationResult::new();
136 let source = code.source();
137 let file_str = file.to_string_lossy().to_string();
138
139 for issue in self.check_useeffect_fetching(source, &file_str) {
140 result.add_issue(issue);
141 }
142 for issue in self.check_missing_keys(source, &file_str) {
143 result.add_issue(issue);
144 }
145
146 Ok(result)
147 }
148
149 fn validate_raw(&self, source: &str, file: &Path) -> Result<ValidationResult> {
150 let parsed = ParsedCode::Generic(source.to_string());
151 self.validate_parsed(&parsed, file)
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_performance_validator_valid() {
161 let validator = PerformanceValidator::default_config();
162 let code = r#"function Button({ children }) { return <button>{children}</button>; }"#;
163 let result = validator.validate_raw(code, Path::new("Button.tsx")).unwrap();
164 assert!(result.passed);
165 }
166
167 #[test]
168 fn test_perf_useeffect_fetch() {
169 let validator = PerformanceValidator::default_config();
170 let code = r#"useEffect(() => { fetch('/api/data').then(r => r.json()); }, []);"#;
171 let result = validator.validate_raw(code, Path::new("Data.tsx")).unwrap();
172 assert!(!result.passed || result.warning_count() >= 1);
173 }
174
175 #[test]
176 fn test_perf_validator_supports() {
177 let validator = PerformanceValidator::default_config();
178 assert!(validator.supports(Language::JavaScript));
179 assert!(!validator.supports(Language::Rust));
180 }
181}