Files
millionco__react-doctor/native/oxlint/rules/three-require-postprocessing-cleanup.rs

1325 lines
47 KiB
Rust

use oxc_ast::{AstKind, ast::Expression};
use oxc_diagnostics::OxcDiagnostic;
use oxc_semantic::{NodeId, SymbolId};
use crate::{
context::LintContext,
rule::{Rule, RuleCategory, RuleInfo, RuleMeta},
};
const R3F_ROOT_REACT_MODULES: [&str; 5] = [
"react",
"react-dom",
"preact/compat",
"preact/hooks",
"@wordpress/element",
];
const THREE_POSTPROCESSING_EFFECT_HOOK_NAMES: [&str; 3] =
["useEffect", "useInsertionEffect", "useLayoutEffect"];
const THREE_POSTPROCESSING_BORROWING_METHOD_NAMES: [&str; 0] = [];
const THREE_POSTPROCESSING_BARREL_MODULES: [&str; 5] = [
"three/addons",
"three/addons/Addons",
"three/addons/Addons.js",
"three/examples/jsm/Addons",
"three/examples/jsm/Addons.js",
];
const THREE_RESOURCE_OWNING_PASS_NAMES: [&str; 26] = [
"AdaptiveToneMappingPass",
"AfterimagePass",
"BloomPass",
"BokehPass",
"CubeTexturePass",
"DotScreenPass",
"FilmPass",
"FXAAPass",
"GTAOPass",
"GlitchPass",
"HalftonePass",
"LUTPass",
"OutlinePass",
"OutputPass",
"RenderPixelatedPass",
"RenderTransitionPass",
"SAOPass",
"SMAAPass",
"SSAARenderPass",
"SSAOPass",
"SSRPass",
"SavePass",
"ShaderPass",
"TAARenderPass",
"TexturePass",
"UnrealBloomPass",
];
const THREE_PASSES_WITH_BASE_RELEASE_DISPOSAL: [&str; 6] = [
"AdaptiveToneMappingPass",
"OutlinePass",
"SSAARenderPass",
"SSAOPass",
"SSRPass",
"UnrealBloomPass",
];
const MESSAGE: &str = "This component-owned postprocessing resource has no provable React cleanup, so its GPU resources can survive dependency changes or unmount";
#[derive(Debug, Default, Clone)]
pub struct ThreeRequirePostprocessingCleanup;
impl RuleMeta for ThreeRequirePostprocessingCleanup {
const NAME: &'static str = "three-require-postprocessing-cleanup";
const PLUGIN: &'static str = "react_doctor_native";
const CATEGORY: RuleCategory = RuleCategory::Correctness;
const VERSION: &'static str = "0.1.0";
const INFO: RuleInfo = RuleInfo {
short_description: "Require cleanup for component-owned postprocessing resources.",
};
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ThreePostprocessingCreationKind {
Effect,
Reactive,
Render,
Stable,
}
struct ThreePostprocessingLifecycle {
access_path: R3fOwnedRootAccessPath,
creation_kind: ThreePostprocessingCreationKind,
has_eager_hook_allocation: bool,
owner_id: NodeId,
owner_symbol_ids: rustc_hash::FxHashSet<SymbolId>,
resource_symbol_ids: rustc_hash::FxHashSet<SymbolId>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ThreePostprocessingResourceKind {
Pass,
PmndrsComposer,
ThreeComposer,
}
struct ThreePostprocessingAllocation {
node_id: NodeId,
resource_kind: ThreePostprocessingResourceKind,
}
struct ThreePostprocessingResource {
allocation: ThreePostprocessingAllocation,
lifecycle: ThreePostprocessingLifecycle,
was_reported: bool,
}
struct ThreePostprocessingEffectEntry {
callback_id: NodeId,
call_id: NodeId,
owner_id: Option<NodeId>,
has_unconditional_registration: bool,
}
enum ThreePostprocessingDependencyStatus {
Valid,
Invalid,
Unknown,
}
impl Rule for ThreeRequirePostprocessingCleanup {
fn run_once<'a>(&self, ctx: &LintContext<'a>) {
let allocation_candidates = ctx
.nodes()
.iter()
.filter_map(|node| {
let AstKind::NewExpression(allocation) = node.kind() else {
return None;
};
let constructor_name = three_postprocessing_candidate_api_name(
&allocation.callee,
ctx,
&mut Vec::new(),
)?;
(constructor_name == "EffectComposer"
|| THREE_RESOURCE_OWNING_PASS_NAMES.contains(&constructor_name.as_str()))
.then_some((node.id(), constructor_name))
})
.collect::<Vec<_>>();
if allocation_candidates.is_empty() {
return;
}
let analysis = build_possible_static_property_write_analysis(ctx);
let allocations = allocation_candidates
.into_iter()
.filter_map(|(node_id, constructor_name)| {
let AstKind::NewExpression(allocation) = ctx.nodes().get_node(node_id).kind()
else {
return None;
};
let resource_kind = three_postprocessing_resource_kind(
&allocation.callee,
&constructor_name,
&analysis,
ctx,
)?;
Some(ThreePostprocessingAllocation {
node_id,
resource_kind,
})
})
.collect::<Vec<_>>();
if allocations.is_empty() {
return;
}
let node_index = build_local_callback_nearest_function_node_index(ctx);
let mut resolution_cache = LocalFunctionResolutionCache::default();
let effect_entries =
three_postprocessing_effect_entries(&analysis, &node_index, ctx, &mut resolution_cache);
let mut resources = Vec::new();
for allocation_record in allocations {
let allocation = ctx.nodes().get_node(allocation_record.node_id);
let lifecycle =
r3f_analyze_owned_root_lifecycle(allocation, &analysis, &node_index, ctx)
.map(|lifecycle| ThreePostprocessingLifecycle {
access_path: lifecycle.access_path,
creation_kind: if lifecycle.is_stable {
ThreePostprocessingCreationKind::Stable
} else {
ThreePostprocessingCreationKind::Reactive
},
has_eager_hook_allocation: false,
owner_id: lifecycle.owner_id,
owner_symbol_ids: lifecycle.owner_symbol_ids,
resource_symbol_ids: lifecycle.resource_symbol_ids,
})
.or_else(|| {
three_postprocessing_eager_hook_lifecycle(allocation, &analysis, ctx)
})
.or_else(|| {
three_postprocessing_direct_lifecycle(allocation, &effect_entries, ctx)
});
let Some(lifecycle) = lifecycle else {
continue;
};
let was_reported = lifecycle.has_eager_hook_allocation
|| three_postprocessing_has_unstable_identity(&lifecycle, allocation, ctx);
resources.push(ThreePostprocessingResource {
allocation: allocation_record,
lifecycle,
was_reported,
});
}
resources.sort_by_key(|resource| {
resource.allocation.resource_kind == ThreePostprocessingResourceKind::Pass
});
let owned_three_composer_lifecycles = resources
.iter()
.filter(|resource| {
resource.allocation.resource_kind == ThreePostprocessingResourceKind::ThreeComposer
})
.filter(|resource| {
resource.was_reported
|| !three_postprocessing_has_unknown_transfer(
&resource.lifecycle,
&[],
false,
&analysis,
ctx,
)
})
.map(|resource| &resource.lifecycle)
.collect::<Vec<_>>();
for resource in &resources {
let allocation = ctx.nodes().get_node(resource.allocation.node_id);
if resource.was_reported {
ctx.diagnostic(OxcDiagnostic::warn(MESSAGE).with_label(allocation.span()));
continue;
}
if three_postprocessing_has_unknown_transfer(
&resource.lifecycle,
&owned_three_composer_lifecycles,
resource.allocation.resource_kind == ThreePostprocessingResourceKind::Pass,
&analysis,
ctx,
) || three_postprocessing_has_cleanup(
allocation,
&resource.lifecycle,
&effect_entries,
&analysis,
&node_index,
ctx,
&mut resolution_cache,
) {
continue;
}
ctx.diagnostic(OxcDiagnostic::warn(MESSAGE).with_label(allocation.span()));
}
}
}
fn three_postprocessing_candidate_api_name<'a>(
expression: &Expression<'a>,
ctx: &LintContext<'a>,
visited_symbol_ids: &mut Vec<SymbolId>,
) -> Option<String> {
let expression = expression.get_inner_expression();
if let Some(member) = expression.as_member_expression() {
return static_member_expression_property_name(member).map(str::to_string);
}
let Expression::Identifier(identifier) = expression else {
return None;
};
let symbol_id = identifier_symbol_id_with_lexical_fallback(identifier, ctx)?;
if visited_symbol_ids.contains(&symbol_id) {
return None;
}
visited_symbol_ids.push(symbol_id);
let declaration = ctx.symbol_declaration(symbol_id);
if let AstKind::TSImportEqualsDeclaration(import_equals) = declaration.kind() {
let oxc_ast::ast::TSModuleReference::QualifiedName(qualified_name) =
&import_equals.module_reference
else {
return None;
};
return Some(qualified_name.right.name.to_string());
}
if let AstKind::VariableDeclarator(declarator) = declaration.kind() {
if !matches!(
ctx.nodes().parent_node(declaration.id()).kind(),
AstKind::VariableDeclaration(variable_declaration)
if variable_declaration.kind.is_const()
) {
return None;
}
if declarator
.id
.get_binding_identifier()
.is_some_and(|binding| binding.symbol_id() == symbol_id)
{
return three_postprocessing_candidate_api_name(
declarator.init.as_ref()?,
ctx,
visited_symbol_ids,
);
}
return destructured_binding_provenance(
&declarator.id,
symbol_id,
declarator.init.as_ref(),
)
.map(|(property_name, _)| property_name);
}
ctx.module_record().import_entries.iter().find_map(|entry| {
if ctx
.scoping()
.get_root_binding(entry.local_name.name().into())
!= Some(symbol_id)
{
return None;
}
let crate::module_record::ImportImportName::Name(imported_name) = &entry.import_name else {
return None;
};
Some(imported_name.name().to_string())
})
}
fn three_postprocessing_resource_kind<'a>(
callee: &Expression<'a>,
api_name: &str,
analysis: &PossibleStaticPropertyWriteAnalysis,
ctx: &LintContext<'a>,
) -> Option<ThreePostprocessingResourceKind> {
if api_name == "EffectComposer"
&& three_postprocessing_reference_matches_sources(
callee,
api_name,
&["postprocessing"],
analysis,
ctx,
)
{
return Some(ThreePostprocessingResourceKind::PmndrsComposer);
}
let direct_module_sources = [
format!("three/addons/postprocessing/{api_name}"),
format!("three/addons/postprocessing/{api_name}.js"),
format!("three/examples/jsm/postprocessing/{api_name}"),
format!("three/examples/jsm/postprocessing/{api_name}.js"),
];
let direct_module_source_refs = direct_module_sources.each_ref().map(String::as_str);
let is_barrel = three_postprocessing_reference_matches_sources(
callee,
api_name,
&THREE_POSTPROCESSING_BARREL_MODULES,
analysis,
ctx,
);
if !is_barrel
&& !three_postprocessing_reference_matches_sources(
callee,
api_name,
&direct_module_source_refs,
analysis,
ctx,
)
{
return None;
}
let is_composer = api_name == "EffectComposer";
if !is_composer && !THREE_RESOURCE_OWNING_PASS_NAMES.contains(&api_name) {
return None;
}
let resource_release = three_postprocessing_resource_disposal_release(api_name);
let required_release = if is_barrel {
resource_release.max(158)
} else {
resource_release
};
has_capability(ctx, &format!("three:{required_release}")).then_some(if is_composer {
ThreePostprocessingResourceKind::ThreeComposer
} else {
ThreePostprocessingResourceKind::Pass
})
}
fn three_postprocessing_reference_matches_sources<'a>(
callee: &Expression<'a>,
api_name: &str,
module_sources: &[&str],
analysis: &PossibleStaticPropertyWriteAnalysis,
ctx: &LintContext<'a>,
) -> bool {
module_api_reference_matches(callee, api_name, module_sources, analysis, ctx)
|| type_import_module_api_reference_matches(callee, api_name, module_sources, analysis, ctx)
}
fn three_postprocessing_resource_disposal_release(api_name: &str) -> u16 {
match api_name {
"RenderPixelatedPass" => 147,
"OutputPass" => 153,
"GTAOPass" => 160,
"RenderTransitionPass" => 164,
"FXAAPass" => 177,
"EffectComposer" => 146,
api_name if THREE_PASSES_WITH_BASE_RELEASE_DISPOSAL.contains(&api_name) => 145,
_ => 146,
}
}
fn three_postprocessing_direct_lifecycle<'a>(
allocation: &crate::AstNode<'a>,
effect_entries: &[ThreePostprocessingEffectEntry],
ctx: &LintContext<'a>,
) -> Option<ThreePostprocessingLifecycle> {
let allocation_root = transparent_expression_root(allocation, ctx);
let declaration = ctx.nodes().parent_node(allocation_root.id());
let AstKind::VariableDeclarator(declarator) = declaration.kind() else {
return None;
};
if declarator
.init
.as_ref()
.is_none_or(|initializer| initializer.span() != allocation_root.span())
{
return None;
}
let source_symbol_id = declarator.id.get_binding_identifier()?.symbol_id();
let allocation_function_id = local_callback_nearest_function_id(allocation.id(), ctx)?;
if is_node_conditionally_executed(allocation, allocation_function_id, ctx) {
return None;
}
let allocation_effect = effect_entries
.iter()
.find(|entry| entry.callback_id == allocation_function_id);
let (owner_id, creation_kind) = if let Some(effect) = allocation_effect {
(effect.owner_id?, ThreePostprocessingCreationKind::Effect)
} else {
let owner = find_render_phase_component_or_hook(allocation, ctx)?;
if crate::ast_util::get_enclosing_function(ctx.symbol_declaration(source_symbol_id), ctx)
.map(crate::AstNode::id)
!= Some(owner.id())
{
return None;
}
(owner.id(), ThreePostprocessingCreationKind::Render)
};
let symbol_ids = r3f_root_collect_alias_symbol_ids(source_symbol_id, ctx);
Some(ThreePostprocessingLifecycle {
access_path: R3fOwnedRootAccessPath::Direct,
creation_kind,
has_eager_hook_allocation: false,
owner_id,
owner_symbol_ids: symbol_ids.clone(),
resource_symbol_ids: symbol_ids,
})
}
fn r3f_root_collect_alias_symbol_ids(
source_symbol_id: SymbolId,
ctx: &LintContext<'_>,
) -> rustc_hash::FxHashSet<SymbolId> {
let mut symbol_ids = rustc_hash::FxHashSet::from_iter([source_symbol_id]);
let mut pending_symbol_ids = vec![source_symbol_id];
while let Some(symbol_id) = pending_symbol_ids.pop() {
for reference in ctx.scoping().get_resolved_references(symbol_id) {
let reference_node = ctx.nodes().get_node(reference.node_id());
let reference_root = transparent_expression_root(reference_node, ctx);
let declaration = ctx.nodes().parent_node(reference_root.id());
let AstKind::VariableDeclarator(declarator) = declaration.kind() else {
continue;
};
if declarator
.init
.as_ref()
.is_none_or(|initializer| initializer.span() != reference_root.span())
{
continue;
}
let Some(alias_binding) = declarator.id.get_binding_identifier() else {
continue;
};
let alias_symbol_id = alias_binding.symbol_id();
if !matches!(
ctx.nodes().parent_node(declaration.id()).kind(),
AstKind::VariableDeclaration(variable_declaration)
if variable_declaration.kind.is_const()
) || ctx
.scoping()
.get_resolved_references(alias_symbol_id)
.any(oxc_semantic::Reference::is_write)
{
continue;
}
if symbol_ids.insert(alias_symbol_id) {
pending_symbol_ids.push(alias_symbol_id);
}
}
}
symbol_ids
}
fn three_postprocessing_eager_hook_lifecycle<'a>(
allocation: &crate::AstNode<'a>,
analysis: &PossibleStaticPropertyWriteAnalysis,
ctx: &LintContext<'a>,
) -> Option<ThreePostprocessingLifecycle> {
let mut current = transparent_expression_root(allocation, ctx);
for parent in ctx.nodes().ancestors(current.id()) {
if matches!(
parent.kind(),
AstKind::Function(_) | AstKind::ArrowFunctionExpression(_)
) {
return None;
}
if let AstKind::CallExpression(call_expression) = parent.kind()
&& call_expression
.arguments
.first()
.is_some_and(|argument| argument.span() == current.span())
{
let is_ref = r3f_owned_root_react_api_matches(call_expression, "useRef", analysis, ctx);
let is_state =
r3f_owned_root_react_api_matches(call_expression, "useState", analysis, ctx);
if !is_ref && !is_state {
current = parent;
continue;
}
let owner_id = find_render_phase_component_or_hook(parent, ctx)?.id();
let untracked_lifecycle = || ThreePostprocessingLifecycle {
access_path: R3fOwnedRootAccessPath::Direct,
creation_kind: ThreePostprocessingCreationKind::Stable,
has_eager_hook_allocation: true,
owner_id,
owner_symbol_ids: rustc_hash::FxHashSet::default(),
resource_symbol_ids: rustc_hash::FxHashSet::default(),
};
let call_root = transparent_expression_root(parent, ctx);
let declaration = ctx.nodes().parent_node(call_root.id());
let AstKind::VariableDeclarator(declarator) = declaration.kind() else {
return Some(untracked_lifecycle());
};
if declarator
.init
.as_ref()
.is_none_or(|initializer| initializer.span() != call_root.span())
{
return Some(untracked_lifecycle());
}
let binding = if is_ref {
let Some(binding) = declarator.id.get_binding_identifier() else {
return Some(untracked_lifecycle());
};
binding
} else {
let oxc_ast::ast::BindingPattern::ArrayPattern(pattern) = &declarator.id else {
return Some(untracked_lifecycle());
};
let Some(binding) = pattern
.elements
.first()
.and_then(Option::as_ref)
.and_then(oxc_ast::ast::BindingPattern::get_binding_identifier)
else {
return Some(untracked_lifecycle());
};
binding
};
if crate::ast_util::get_enclosing_function(declaration, ctx).map(crate::AstNode::id)
!= Some(owner_id)
{
return Some(untracked_lifecycle());
}
let owner_symbol_ids = r3f_root_collect_alias_symbol_ids(binding.symbol_id(), ctx);
let access_path = if is_ref {
R3fOwnedRootAccessPath::Object("current".to_string())
} else {
R3fOwnedRootAccessPath::Direct
};
let resource_symbol_ids =
r3f_owned_root_collect_resource_aliases(&owner_symbol_ids, &access_path, ctx);
return Some(ThreePostprocessingLifecycle {
access_path,
creation_kind: ThreePostprocessingCreationKind::Stable,
has_eager_hook_allocation: true,
owner_id,
owner_symbol_ids,
resource_symbol_ids,
});
}
current = parent;
}
None
}
fn three_postprocessing_effect_entries<'a>(
analysis: &PossibleStaticPropertyWriteAnalysis,
node_index: &LocalCallbackNearestFunctionNodeIndex,
ctx: &LintContext<'a>,
resolution_cache: &mut LocalFunctionResolutionCache,
) -> Vec<ThreePostprocessingEffectEntry> {
let mut entries = Vec::new();
for node in ctx.nodes().iter() {
let AstKind::CallExpression(call_expression) = node.kind() else {
continue;
};
if !THREE_POSTPROCESSING_EFFECT_HOOK_NAMES
.iter()
.any(|hook_name| {
r3f_owned_root_react_api_matches(call_expression, hook_name, analysis, ctx)
})
{
continue;
}
let Some(callback_expression) = call_expression
.arguments
.first()
.and_then(oxc_ast::ast::Argument::as_expression)
else {
continue;
};
let Some(callback_id) =
exact_local_function_id(callback_expression, ctx, &mut Vec::new(), resolution_cache)
else {
continue;
};
let owner_id = find_render_phase_component_or_hook(node, ctx).map(crate::AstNode::id);
entries.push(ThreePostprocessingEffectEntry {
callback_id,
call_id: node.id(),
owner_id,
has_unconditional_registration: owner_id.is_some_and(|owner_id| {
three_postprocessing_execution_is_guaranteed(node, owner_id, node_index, ctx)
}),
});
}
entries
}
fn three_postprocessing_expression_matches(
expression: &Expression<'_>,
lifecycle: &ThreePostprocessingLifecycle,
ctx: &LintContext<'_>,
) -> bool {
let expression = expression.get_inner_expression();
if let Expression::Identifier(identifier) = expression {
return ctx
.scoping()
.get_reference(identifier.reference_id())
.symbol_id()
.is_some_and(|symbol_id| lifecycle.resource_symbol_ids.contains(&symbol_id));
}
let Some(member) = expression.as_member_expression() else {
return false;
};
let Expression::Identifier(identifier) = member.object().get_inner_expression() else {
return false;
};
ctx.scoping()
.get_reference(identifier.reference_id())
.symbol_id()
.is_some_and(|symbol_id| lifecycle.owner_symbol_ids.contains(&symbol_id))
&& r3f_owned_root_member_matches_path(member, &lifecycle.access_path)
}
fn three_postprocessing_expression_matches_owner(
expression: &Expression<'_>,
lifecycle: &ThreePostprocessingLifecycle,
ctx: &LintContext<'_>,
) -> bool {
three_postprocessing_expression_matches(expression, lifecycle, ctx)
|| matches!(expression.get_inner_expression(), Expression::Identifier(identifier)
if ctx.scoping().get_reference(identifier.reference_id()).symbol_id().is_some_and(
|symbol_id| lifecycle.owner_symbol_ids.contains(&symbol_id)
))
}
fn three_postprocessing_resource_access<'a, 'b>(
reference_node: &'b crate::AstNode<'a>,
reference_symbol_id: SymbolId,
lifecycle: &ThreePostprocessingLifecycle,
ctx: &'b LintContext<'a>,
) -> Option<&'b crate::AstNode<'a>> {
let reference_root = transparent_expression_root(reference_node, ctx);
if lifecycle.resource_symbol_ids.contains(&reference_symbol_id) {
return Some(reference_root);
}
if matches!(lifecycle.access_path, R3fOwnedRootAccessPath::Direct) {
return None;
}
let member_node = ctx.nodes().parent_node(reference_root.id());
let member = member_node.kind().as_member_expression_kind()?;
(member.object().span() == reference_root.span()
&& r3f_owned_root_member_node_matches_path(member_node, &lifecycle.access_path))
.then_some(member_node)
}
fn three_postprocessing_has_unstable_identity<'a>(
lifecycle: &ThreePostprocessingLifecycle,
allocation: &crate::AstNode<'a>,
ctx: &LintContext<'a>,
) -> bool {
let symbol_ids = lifecycle
.owner_symbol_ids
.iter()
.chain(&lifecycle.resource_symbol_ids)
.copied()
.collect::<rustc_hash::FxHashSet<_>>();
for symbol_id in symbol_ids {
for reference in ctx.scoping().get_resolved_references(symbol_id) {
let reference_node = ctx.nodes().get_node(reference.node_id());
let Some(resource_access) =
three_postprocessing_resource_access(reference_node, symbol_id, lifecycle, ctx)
else {
continue;
};
if r3f_owned_root_access_has_non_allocation_identity_write(
resource_access,
allocation,
ctx,
) {
return true;
}
}
}
false
}
fn three_postprocessing_has_unknown_transfer<'a>(
lifecycle: &ThreePostprocessingLifecycle,
owned_three_composer_lifecycles: &[&ThreePostprocessingLifecycle],
can_be_borrowed_by_composer: bool,
analysis: &PossibleStaticPropertyWriteAnalysis,
ctx: &LintContext<'a>,
) -> bool {
for symbol_id in lifecycle
.owner_symbol_ids
.iter()
.chain(&lifecycle.resource_symbol_ids)
.copied()
{
for reference in ctx.scoping().get_resolved_references(symbol_id) {
let reference_node = ctx.nodes().get_node(reference.node_id());
let resource_access =
three_postprocessing_resource_access(reference_node, symbol_id, lifecycle, ctx);
let Some(resource_access) = resource_access else {
if matches!(lifecycle.access_path, R3fOwnedRootAccessPath::Direct)
|| !lifecycle.owner_symbol_ids.contains(&symbol_id)
{
continue;
}
let reference_root = transparent_expression_root(reference_node, ctx);
let parent = ctx.nodes().parent_node(reference_root.id());
if matches!(parent.kind(), AstKind::VariableDeclarator(declarator)
if declarator.init.as_ref().is_some_and(|initializer| initializer.span() == reference_root.span()))
|| three_postprocessing_is_effect_dependency(reference_root, analysis, ctx)
|| parent
.kind()
.as_member_expression_kind()
.is_some_and(|member| {
member.object().span() == reference_root.span()
&& member.static_property_name().is_some()
})
{
continue;
}
return true;
};
let resource_root = transparent_expression_root(resource_access, ctx);
let parent = ctx.nodes().parent_node(resource_root.id());
if matches!(parent.kind(), AstKind::VariableDeclarator(declarator)
if declarator.init.as_ref().is_some_and(|initializer| initializer.span() == resource_root.span())
&& declarator.id.get_binding_identifier().is_some())
|| three_postprocessing_is_effect_dependency(resource_root, analysis, ctx)
{
continue;
}
if three_postprocessing_crosses_custom_jsx_boundary(resource_root, ctx) {
return true;
}
let is_resource_member_access = parent
.kind()
.as_member_expression_kind()
.is_some_and(|member| member.object().span() == resource_root.span());
if three_postprocessing_is_returned_from_owner(resource_root, lifecycle.owner_id, ctx)
&& !is_resource_member_access
&& !three_postprocessing_is_inside_jsx(resource_root, ctx)
{
return true;
}
if let Some((call_expression, argument_span)) =
three_postprocessing_containing_call_argument(resource_root, ctx)
{
if can_be_borrowed_by_composer
&& argument_span == resource_root.span()
&& three_postprocessing_is_borrowed_by_owned_composer(
call_expression,
owned_three_composer_lifecycles,
ctx,
)
{
continue;
}
let method_name = call_expression
.callee
.get_inner_expression()
.as_member_expression()
.and_then(static_member_expression_property_name);
if argument_span == resource_root.span()
&& method_name.is_none_or(|method_name| {
!THREE_POSTPROCESSING_BORROWING_METHOD_NAMES.contains(&method_name)
})
{
return true;
}
continue;
}
if is_resource_member_access {
continue;
}
let mut current = resource_root;
for ancestor in ctx.nodes().ancestors(resource_root.id()) {
if matches!(
ancestor.kind(),
AstKind::Function(_) | AstKind::ArrowFunctionExpression(_)
) {
break;
}
let is_wrapping_transfer = matches!(ancestor.kind(), AstKind::AssignmentExpression(assignment)
if assignment.right.span() == current.span())
|| matches!(ancestor.kind(), AstKind::ObjectProperty(property)
if property.value.span() == current.span())
|| matches!(ancestor.kind(), AstKind::ArrayExpression(_));
if is_wrapping_transfer {
if matches!(
ancestor.kind(),
AstKind::ObjectProperty(_) | AstKind::ArrayExpression(_)
) && three_postprocessing_is_nested_in_owned_memo_jsx(
resource_root,
lifecycle.owner_id,
analysis,
ctx,
) {
break;
}
return true;
}
current = ancestor;
}
}
}
false
}
fn three_postprocessing_is_inside_jsx(
reference: &crate::AstNode<'_>,
ctx: &LintContext<'_>,
) -> bool {
for ancestor in ctx.nodes().ancestors(reference.id()) {
if matches!(
ancestor.kind(),
AstKind::Function(_) | AstKind::ArrowFunctionExpression(_)
) {
break;
}
if matches!(ancestor.kind(), AstKind::JSXExpressionContainer(_)) {
return true;
}
}
false
}
fn three_postprocessing_crosses_custom_jsx_boundary<'a>(
reference: &crate::AstNode<'a>,
ctx: &LintContext<'a>,
) -> bool {
let reference_root = transparent_expression_root(reference, ctx);
let parent = ctx.nodes().parent_node(reference_root.id());
if parent
.kind()
.as_member_expression_kind()
.is_some_and(|member| member.object().span() == reference_root.span())
{
return false;
}
for ancestor in ctx.nodes().ancestors(reference.id()) {
if matches!(
ancestor.kind(),
AstKind::Function(_) | AstKind::ArrowFunctionExpression(_)
) {
break;
}
let AstKind::JSXAttribute(_) = ancestor.kind() else {
continue;
};
let opening_element = ctx.nodes().parent_node(ancestor.id());
let AstKind::JSXOpeningElement(opening_element) = opening_element.kind() else {
return true;
};
let oxc_ast::ast::JSXElementName::Identifier(element_name) = &opening_element.name else {
return true;
};
return element_name.name.contains('-')
|| element_name
.name
.chars()
.next()
.is_none_or(char::is_uppercase);
}
false
}
fn three_postprocessing_is_nested_in_owned_memo_jsx<'a>(
reference: &crate::AstNode<'a>,
owner_id: NodeId,
analysis: &PossibleStaticPropertyWriteAnalysis,
ctx: &LintContext<'a>,
) -> bool {
let Some(callback_id) = local_callback_nearest_function_id(reference.id(), ctx) else {
return false;
};
if callback_id == owner_id {
return false;
}
let callback = ctx.nodes().get_node(callback_id);
let callback_root = transparent_expression_root(callback, ctx);
let memo_call_node = ctx.nodes().parent_node(callback_root.id());
let AstKind::CallExpression(memo_call) = memo_call_node.kind() else {
return false;
};
if memo_call
.arguments
.first()
.is_none_or(|argument| argument.span() != callback_root.span())
|| !r3f_owned_root_react_api_matches(memo_call, "useMemo", analysis, ctx)
|| find_render_phase_component_or_hook(memo_call_node, ctx).map(crate::AstNode::id)
!= Some(owner_id)
{
return false;
}
let memo_root = transparent_expression_root(memo_call_node, ctx);
let declaration = ctx.nodes().parent_node(memo_root.id());
let AstKind::VariableDeclarator(declarator) = declaration.kind() else {
return false;
};
if declarator
.init
.as_ref()
.is_none_or(|initializer| initializer.span() != memo_root.span())
{
return false;
}
let Some(binding) = declarator.id.get_binding_identifier() else {
return false;
};
let references = ctx
.scoping()
.get_resolved_references(binding.symbol_id())
.collect::<Vec<_>>();
!references.is_empty()
&& references.iter().all(|memo_reference| {
let memo_reference_node = ctx.nodes().get_node(memo_reference.node_id());
three_postprocessing_is_inside_jsx(memo_reference_node, ctx)
&& !three_postprocessing_crosses_custom_jsx_boundary(memo_reference_node, ctx)
})
}
fn three_postprocessing_is_borrowed_by_owned_composer(
call_expression: &oxc_ast::ast::CallExpression<'_>,
owned_three_composer_lifecycles: &[&ThreePostprocessingLifecycle],
ctx: &LintContext<'_>,
) -> bool {
let Some(callee) = call_expression
.callee
.get_inner_expression()
.as_member_expression()
else {
return false;
};
if !matches!(
static_member_expression_property_name(callee),
Some("addPass" | "insertPass")
) {
return false;
}
owned_three_composer_lifecycles
.iter()
.any(|lifecycle| three_postprocessing_expression_matches(callee.object(), lifecycle, ctx))
}
fn three_postprocessing_is_effect_dependency<'a>(
reference: &crate::AstNode<'a>,
analysis: &PossibleStaticPropertyWriteAnalysis,
ctx: &LintContext<'a>,
) -> bool {
let Some((call_expression, argument_span)) =
three_postprocessing_containing_call_argument(reference, ctx)
else {
return false;
};
call_expression
.arguments
.get(1)
.is_some_and(|dependency| dependency.span() == argument_span)
&& THREE_POSTPROCESSING_EFFECT_HOOK_NAMES
.iter()
.any(|hook_name| {
r3f_owned_root_react_api_matches(call_expression, hook_name, analysis, ctx)
})
}
fn three_postprocessing_is_returned_from_owner(
reference: &crate::AstNode<'_>,
owner_id: NodeId,
ctx: &LintContext<'_>,
) -> bool {
local_callback_nearest_function_id(reference.id(), ctx) == Some(owner_id)
&& ctx
.nodes()
.ancestors(reference.id())
.take_while(|ancestor| ancestor.id() != owner_id)
.any(|ancestor| matches!(ancestor.kind(), AstKind::ReturnStatement(_)))
}
fn three_postprocessing_containing_call_argument<'a, 'b>(
reference: &'b crate::AstNode<'a>,
ctx: &'b LintContext<'a>,
) -> Option<(&'b oxc_ast::ast::CallExpression<'a>, oxc_span::Span)> {
let mut current = reference;
for parent in ctx.nodes().ancestors(reference.id()) {
if matches!(
parent.kind(),
AstKind::Function(_) | AstKind::ArrowFunctionExpression(_)
) {
return None;
}
if let AstKind::CallExpression(call_expression) = parent.kind() {
return call_expression
.arguments
.iter()
.any(|argument| argument.span() == current.span())
.then_some((call_expression, current.span()));
}
current = parent;
}
None
}
#[allow(clippy::too_many_arguments)]
fn three_postprocessing_has_cleanup<'a>(
allocation: &crate::AstNode<'a>,
lifecycle: &ThreePostprocessingLifecycle,
effect_entries: &[ThreePostprocessingEffectEntry],
analysis: &PossibleStaticPropertyWriteAnalysis,
node_index: &LocalCallbackNearestFunctionNodeIndex,
ctx: &LintContext<'a>,
resolution_cache: &mut LocalFunctionResolutionCache,
) -> bool {
let mut has_unknown_cleanup = false;
let allocation_effect_call_id = (lifecycle.creation_kind
== ThreePostprocessingCreationKind::Effect)
.then(|| local_callback_nearest_function_id(allocation.id(), ctx))
.flatten()
.and_then(|callback_id| {
effect_entries
.iter()
.find(|entry| entry.callback_id == callback_id)
.map(|entry| entry.call_id)
});
for effect in effect_entries {
if effect.owner_id != Some(lifecycle.owner_id)
|| !effect.has_unconditional_registration
|| allocation_effect_call_id
.is_some_and(|allocation_call_id| allocation_call_id != effect.call_id)
{
continue;
}
let matching_returns = three_postprocessing_matching_cleanup_returns(
effect.callback_id,
lifecycle,
analysis,
node_index,
ctx,
resolution_cache,
);
let cleanup_matches = if lifecycle.creation_kind == ThreePostprocessingCreationKind::Effect
&& local_callback_nearest_function_id(allocation.id(), ctx) == Some(effect.callback_id)
{
do_nodes_cover_every_path_after_node(
allocation,
&matching_returns,
ctx.nodes().get_node(effect.callback_id),
ctx,
)
} else {
three_postprocessing_function_returns_cleanup_on_every_path(
effect.callback_id,
&matching_returns,
node_index,
ctx,
)
};
if !cleanup_matches {
continue;
}
match three_postprocessing_dependency_status(effect, lifecycle, ctx) {
ThreePostprocessingDependencyStatus::Valid => return true,
ThreePostprocessingDependencyStatus::Unknown => has_unknown_cleanup = true,
ThreePostprocessingDependencyStatus::Invalid => {}
}
}
has_unknown_cleanup
}
fn three_postprocessing_dependency_status(
effect: &ThreePostprocessingEffectEntry,
lifecycle: &ThreePostprocessingLifecycle,
ctx: &LintContext<'_>,
) -> ThreePostprocessingDependencyStatus {
let AstKind::CallExpression(call_expression) = ctx.nodes().get_node(effect.call_id).kind()
else {
return ThreePostprocessingDependencyStatus::Invalid;
};
let Some(dependency_argument) = call_expression.arguments.get(1) else {
return ThreePostprocessingDependencyStatus::Valid;
};
let Some(dependency_expression) = dependency_argument.as_expression() else {
return ThreePostprocessingDependencyStatus::Unknown;
};
let Expression::ArrayExpression(dependencies) = dependency_expression.get_inner_expression()
else {
return ThreePostprocessingDependencyStatus::Unknown;
};
if matches!(
lifecycle.creation_kind,
ThreePostprocessingCreationKind::Stable | ThreePostprocessingCreationKind::Effect
) {
return ThreePostprocessingDependencyStatus::Valid;
}
if dependencies.elements.iter().any(|element| {
element.as_expression().is_some_and(|expression| {
three_postprocessing_expression_matches_owner(expression, lifecycle, ctx)
})
}) {
ThreePostprocessingDependencyStatus::Valid
} else {
ThreePostprocessingDependencyStatus::Invalid
}
}
fn three_postprocessing_matching_cleanup_returns<'a, 'b>(
callback_id: NodeId,
lifecycle: &ThreePostprocessingLifecycle,
analysis: &PossibleStaticPropertyWriteAnalysis,
node_index: &LocalCallbackNearestFunctionNodeIndex,
ctx: &'b LintContext<'a>,
resolution_cache: &mut LocalFunctionResolutionCache,
) -> Vec<&'b crate::AstNode<'a>> {
let mut matching_returns = Vec::new();
for &candidate_id in node_index.node_ids(callback_id) {
let candidate = ctx.nodes().get_node(candidate_id);
let AstKind::ReturnStatement(return_statement) = candidate.kind() else {
continue;
};
let Some(returned_expression) = return_statement.argument.as_ref() else {
continue;
};
if three_postprocessing_cleanup_expression_matches(
returned_expression,
lifecycle,
analysis,
node_index,
ctx,
resolution_cache,
) {
matching_returns.push(candidate);
}
}
if let AstKind::ArrowFunctionExpression(function) = ctx.nodes().get_node(callback_id).kind()
&& let Some(expression) = function.get_expression()
&& three_postprocessing_cleanup_expression_matches(
expression,
lifecycle,
analysis,
node_index,
ctx,
resolution_cache,
)
{
matching_returns.push(ctx.nodes().get_node(expression.node_id()));
}
matching_returns
}
fn three_postprocessing_cleanup_expression_matches<'a>(
expression: &Expression<'a>,
lifecycle: &ThreePostprocessingLifecycle,
analysis: &PossibleStaticPropertyWriteAnalysis,
node_index: &LocalCallbackNearestFunctionNodeIndex,
ctx: &LintContext<'a>,
resolution_cache: &mut LocalFunctionResolutionCache,
) -> bool {
let Some(cleanup_function_id) =
exact_local_function_id(expression, ctx, &mut Vec::new(), resolution_cache)
else {
return false;
};
let mut did_dispose = false;
for_each_analyzed_synchronous_execution_node(
cleanup_function_id,
analysis,
node_index,
ctx,
resolution_cache,
|candidate, _, is_conditional, _| {
if !did_dispose
&& !is_conditional
&& local_callback_nearest_function_id(candidate.id(), ctx).is_some_and(
|function_id| {
three_postprocessing_execution_is_guaranteed(
candidate,
function_id,
node_index,
ctx,
)
},
)
{
did_dispose = three_postprocessing_is_dispose_call(candidate, lifecycle, ctx);
}
},
);
did_dispose
}
fn three_postprocessing_is_dispose_call(
candidate: &crate::AstNode<'_>,
lifecycle: &ThreePostprocessingLifecycle,
ctx: &LintContext<'_>,
) -> bool {
let AstKind::CallExpression(call_expression) = candidate.kind() else {
return false;
};
let Some(member) = call_expression.callee.as_member_expression() else {
return false;
};
static_member_expression_property_name(member) == Some("dispose")
&& three_postprocessing_expression_matches(member.object(), lifecycle, ctx)
}
fn three_postprocessing_function_returns_cleanup_on_every_path(
callback_id: NodeId,
matching_returns: &[&crate::AstNode<'_>],
node_index: &LocalCallbackNearestFunctionNodeIndex,
ctx: &LintContext<'_>,
) -> bool {
if matches!(
ctx.nodes().get_node(callback_id).kind(),
AstKind::ArrowFunctionExpression(function) if function.get_expression().is_some()
) {
return !matching_returns.is_empty();
}
let body_statements = match ctx.nodes().get_node(callback_id).kind() {
AstKind::Function(function) => function
.body
.as_ref()
.map(|body| body.statements.as_slice()),
AstKind::ArrowFunctionExpression(function) => function
.body
.as_function_body()
.map(|body| body.statements.as_slice()),
_ => None,
};
let Some(body_statements) = body_statements else {
return false;
};
let return_count = node_index
.node_ids(callback_id)
.iter()
.filter(|&&node_id| {
matches!(
ctx.nodes().get_node(node_id).kind(),
AstKind::ReturnStatement(_)
)
})
.count();
!matching_returns.is_empty()
&& matching_returns.len() == return_count
&& body_statements.iter().any(statement_always_exits)
}
fn three_postprocessing_execution_is_guaranteed<'a>(
node: &crate::AstNode<'a>,
boundary_id: NodeId,
node_index: &LocalCallbackNearestFunctionNodeIndex,
ctx: &LintContext<'a>,
) -> bool {
if is_node_conditionally_executed(node, boundary_id, ctx)
|| ctx
.nodes()
.ancestors(node.id())
.take_while(|ancestor| ancestor.id() != boundary_id)
.any(|ancestor| {
matches!(
ancestor.kind(),
AstKind::ForStatement(_)
| AstKind::ForInStatement(_)
| AstKind::ForOfStatement(_)
| AstKind::WhileStatement(_)
| AstKind::DoWhileStatement(_)
| AstKind::TryStatement(_)
)
})
{
return false;
}
node_index
.node_ids(boundary_id)
.iter()
.all(|&candidate_id| {
let candidate = ctx.nodes().get_node(candidate_id);
!matches!(
candidate.kind(),
AstKind::ReturnStatement(_) | AstKind::ThrowStatement(_)
) || candidate.span().start >= node.span().start
})
}