Skip to main content

react_auditor/rules/performance/
lazy_load_components.rs

1use oxc_ast::ast::Program;
2use oxc_ast_visit::Visit;
3use oxc_semantic::Semantic;
4
5use crate::rules::{Rule, RuleFinding, RuleMeta, Severity};
6
7pub struct LazyLoadComponents;
8
9const RULE_META: RuleMeta = RuleMeta {
10    id: "lazy-load-components",
11    default_severity: Severity::Warning,
12    category: "performance",
13    description: "Heavy components should be lazy-loaded with React.lazy",
14};
15
16impl Rule for LazyLoadComponents {
17    fn meta(&self) -> &RuleMeta {
18        &RULE_META
19    }
20
21    fn run(&self, program: &Program, _semantic: &Semantic, source_text: &str) -> Vec<RuleFinding> {
22        let mut collector = LazyCollector {
23            findings: Vec::new(),
24            source: source_text,
25        };
26        collector.visit_program(program);
27        collector.findings
28    }
29}
30
31struct LazyCollector<'a> {
32    findings: Vec<RuleFinding>,
33    source: &'a str,
34}
35
36impl<'a> LazyCollector<'a> {
37    fn add_finding(&mut self, start: usize, msg: String) {
38        let line = self.source[..start].lines().count().max(1);
39        let col = start - self.source[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
40        self.findings.push(RuleFinding {
41            line,
42            column: col + 1,
43            message: msg,
44        });
45    }
46}
47
48fn filename_starts_with_uppercase(path: &str) -> bool {
49    path.rsplit('/')
50        .next()
51        .and_then(|name| name.chars().next())
52        .is_some_and(|c| c.is_ascii_uppercase())
53}
54
55impl<'a> Visit<'a> for LazyCollector<'a> {
56    fn visit_import_declaration(&mut self, decl: &oxc_ast::ast::ImportDeclaration<'a>) {
57        let path = decl.source.value.as_str();
58
59        // Check if any imported binding starts with uppercase (PascalCase component)
60        let has_component_binding = decl.specifiers.as_ref().is_some_and(|specifiers| {
61            specifiers.iter().any(|spec| {
62                let local_name = match spec {
63                    oxc_ast::ast::ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
64                        Some(s.local.name.as_str())
65                    }
66                    oxc_ast::ast::ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
67                        Some(s.local.name.as_str())
68                    }
69                    oxc_ast::ast::ImportDeclarationSpecifier::ImportSpecifier(s) => {
70                        Some(s.local.name.as_str())
71                    }
72                };
73                local_name.is_some_and(|n| n.chars().next().is_some_and(|c| c.is_ascii_uppercase()))
74            })
75        });
76
77        // Also check if the module path filename starts with uppercase
78        let path_is_component = filename_starts_with_uppercase(path);
79
80        if has_component_binding || path_is_component {
81            self.add_finding(decl.span.start as usize,
82                format!("Static import of component `{path}` — consider `React.lazy(() => import('{path}'))`"));
83        }
84    }
85}