Skip to main content

oparry_validators/
performance.rs

1//! Performance validator - React performance patterns and best practices
2
3use crate::Validator;
4use oparry_core::{Issue, IssueLevel, Result, ValidationResult};
5use oparry_parser::{ParsedCode, Language};
6use regex::Regex;
7use std::path::Path;
8
9/// Performance validation configuration
10#[derive(Debug, Clone)]
11pub struct PerformanceConfig {
12    /// Require React.memo for components passed as props
13    pub require_memo_for_props: bool,
14    /// Check for missing useMemo/useCallback
15    pub check_hook_usage: bool,
16    /// Detect missing lazy loading
17    pub check_lazy_loading: bool,
18    /// Warn against inline object/function creation in render
19    pub warn_inline_creation: bool,
20    /// Maximum component re-renders threshold
21    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
36/// Performance validator
37pub struct PerformanceValidator {
38    config: PerformanceConfig,
39    component_regex: Regex,
40    useeffect_regex: Regex,
41}
42
43impl PerformanceValidator {
44    /// Create new performance validator
45    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    /// Create with default config
54    pub fn default_config() -> Self {
55        Self::new(PerformanceConfig::default())
56    }
57
58    /// Check for data fetching in useEffect
59    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                // Also check this line for fetch/axios
74                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    /// Check for missing key props in lists
106    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}