use std::path::Path;
use oxc_allocator::Vec as OxcVec;
use oxc_ast::ast::{BindingPatternKind, Declaration, Expression, FunctionBody, Statement};
use oxc_ast_visit::Visit;
use oxc_span::Span;
use crate::{
rules::{Issue, RuleContext, Severity},
utils::offset_to_line_col,
};
pub struct NoComponentInComponent;
impl super::Rule for NoComponentInComponent {
fn name(&self) -> &str {
"no_component_in_component"
}
fn run(&self, ctx: &RuleContext<'_>) -> Vec<Issue> {
let mut visitor = ComponentNestingVisitor {
issues: Vec::new(),
source_text: ctx.source_text,
file_path: ctx.file_path,
};
visitor.scan_statements(&ctx.program.body, 0);
visitor.issues
}
}
struct ComponentNestingVisitor<'a> {
issues: Vec<Issue>,
source_text: &'a str,
file_path: &'a Path,
}
impl<'a> Visit<'a> for ComponentNestingVisitor<'_> {}
impl ComponentNestingVisitor<'_> {
fn scan_statements<'a>(&mut self, stmts: &OxcVec<'a, Statement<'a>>, depth: usize) {
for stmt in stmts {
match stmt {
Statement::FunctionDeclaration(func) => {
let name = func
.id
.as_ref()
.map(|id| id.name.as_str().to_string())
.unwrap_or_default();
if is_pascal_case(&name) {
if depth > 0 {
self.emit(&name, func.span);
}
if let Some(body) = &func.body {
self.scan_function_body(body, depth + 1);
}
} else if let Some(body) = &func.body {
self.scan_function_body(body, depth);
}
}
Statement::VariableDeclaration(var_decl) => {
for declarator in &var_decl.declarations {
let var_name = match &declarator.id.kind {
BindingPatternKind::BindingIdentifier(id) => {
id.name.as_str().to_string()
}
_ => String::new(),
};
if is_pascal_case(&var_name) {
if let Some(init) = &declarator.init {
match init {
Expression::ArrowFunctionExpression(arrow) => {
if depth > 0 {
self.emit(&var_name, arrow.span);
}
self.scan_function_body(&arrow.body, depth + 1);
continue;
}
Expression::FunctionExpression(func) => {
if depth > 0 {
self.emit(&var_name, func.span);
}
if let Some(body) = &func.body {
self.scan_function_body(body, depth + 1);
}
continue;
}
_ => {}
}
}
}
}
}
Statement::ReturnStatement(_)
| Statement::IfStatement(_)
| Statement::BlockStatement(_)
| Statement::ExpressionStatement(_) => {
}
_ => {}
}
}
}
fn scan_function_body<'a>(&mut self, body: &FunctionBody<'a>, depth: usize) {
self.scan_statements(&body.statements, depth);
}
fn emit(&mut self, name: &str, span: Span) {
let (line, col) = offset_to_line_col(self.source_text, span.start);
self.issues.push(Issue {
rule: "no_component_in_component".to_string(),
message: format!(
"Component `{name}` is defined inside another component. \
React creates a new component type on every render, causing the subtree \
to unmount and remount. Move `{name}` outside the parent component."
),
file: self.file_path.to_path_buf(),
line,
column: col,
severity: Severity::Warning,
});
}
}
fn is_pascal_case(name: &str) -> bool {
name.starts_with(|c: char| c.is_ascii_uppercase())
}
#[allow(dead_code)]
fn is_component_declaration(decl: &Declaration<'_>) -> bool {
if let Declaration::FunctionDeclaration(func) = decl {
if let Some(id) = &func.id {
return is_pascal_case(id.name.as_str());
}
}
false
}