react_auditor/rules/performance/
lazy_load_components.rs1use 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 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 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}