Files
millionco__react-doctor/native/oxlint/rules/valtio-no-proxy-read-in-render.rs
2026-08-30 16:09:37 -07:00

1023 lines
35 KiB
Rust

use oxc_ast::{
AstKind,
ast::{
Argument, ArrayAssignmentTarget, AssignmentTarget, AssignmentTargetMaybeDefault,
AssignmentTargetProperty, BindingPattern, ChainElement, Expression, MemberExpression,
ObjectAssignmentTarget, PropertyKey, SimpleAssignmentTarget,
},
};
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_semantic::{NodeId, ScopeId, SymbolId};
use oxc_syntax::operator::UnaryOperator;
use rustc_hash::{FxHashMap, FxHashSet};
use crate::{AstNode, context::LintContext, rule::Rule};
const MESSAGE: &str = "Read this Valtio value from the useSnapshot result during render. Keep proxy reads for callbacks so render uses the tracked, consistent snapshot.";
const VALTIO_MODULES: [&str; 2] = ["valtio", "valtio/react"];
const REACT_RUNTIME_MODULES: [&str; 5] = [
"react",
"react-dom",
"preact/compat",
"preact/hooks",
"@wordpress/element",
];
const DEPENDENCY_ARRAY_HOOK_NAMES: [&str; 5] = [
"useCallback",
"useEffect",
"useInsertionEffect",
"useLayoutEffect",
"useMemo",
];
#[derive(Debug, Default, Clone)]
pub struct ValtioNoProxyReadInRender;
#[derive(Clone, Debug, Eq, PartialEq)]
enum ValtioProxyRoot {
Symbol(SymbolId),
Global(String),
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct ValtioProxyPath {
root: ValtioProxyRoot,
properties: Vec<Option<String>>,
}
#[derive(Clone)]
struct ValtioProxyAliasCapture {
declaration_end: u32,
path: ValtioProxyPath,
}
#[derive(Clone)]
struct ValtioSnapshotTarget {
declaration_end: u32,
owner_id: NodeId,
path: ValtioProxyPath,
snapshot_binding_scopes: Vec<ScopeId>,
}
#[derive(Clone)]
struct ValtioProxyWrite {
position: u32,
owner_id: NodeId,
path: ValtioProxyPath,
}
declare_oxc_lint!(
/// Warns when a Valtio proxy is read during render after it has been snapshotted.
ValtioNoProxyReadInRender,
react_doctor_native,
correctness,
version = "0.1.0",
short_description = "Warns when render reads a Valtio proxy instead of its snapshot.",
);
impl Rule for ValtioNoProxyReadInRender {
fn run_once<'a>(&self, ctx: &LintContext<'a>) {
if !has_capability(ctx, "valtio:1") {
return;
}
if !ctx
.module_record()
.import_entries
.iter()
.any(|entry| VALTIO_MODULES.contains(&entry.module_request.name()))
{
return;
}
let mut snapshot_targets = Vec::new();
let mut identifier_candidates = Vec::new();
let mut proxy_writes = Vec::new();
for node in ctx.nodes().iter() {
match node.kind() {
AstKind::CallExpression(call) => {
if let Some(target) = valtio_snapshot_target(node, call, ctx) {
snapshot_targets.push(target);
}
}
AstKind::IdentifierReference(identifier) => {
identifier_candidates.push((node, identifier));
}
AstKind::AssignmentExpression(assignment) => {
let Some(owner_id) =
find_render_phase_component_or_hook(node, ctx).map(AstNode::id)
else {
continue;
};
valtio_collect_assignment_target_writes(
&assignment.left,
node.span().start,
owner_id,
&mut proxy_writes,
ctx,
);
}
AstKind::UpdateExpression(update) => {
let Some(owner_id) =
find_render_phase_component_or_hook(node, ctx).map(AstNode::id)
else {
continue;
};
valtio_collect_simple_assignment_target_write(
&update.argument,
node.span().start,
owner_id,
&mut proxy_writes,
ctx,
);
}
_ => {}
}
}
let mut reported_expression_ids = FxHashSet::default();
for (identifier_node, _identifier) in identifier_candidates {
let read_expression = valtio_outermost_member_read(identifier_node, ctx);
if reported_expression_ids.contains(&read_expression.id())
|| valtio_is_mutation_only_position(read_expression, ctx)
|| valtio_is_snapshot_argument(read_expression, ctx)
{
continue;
}
let mut alias_captures = FxHashMap::default();
let Some(read_path) = valtio_resolve_proxy_path_from_node(
read_expression,
&mut Vec::new(),
Some(&mut alias_captures),
ctx,
) else {
continue;
};
let read_position = read_expression.span().start;
let Some(owner_id) =
find_render_phase_component_or_hook(read_expression, ctx).map(AstNode::id)
else {
continue;
};
let read_scope = identifier_node.scope_id();
let matching_target = snapshot_targets.iter().find(|target| {
target.owner_id == owner_id
&& target.declaration_end < read_position
&& target.snapshot_binding_scopes.iter().any(|binding_scope| {
ctx.scoping()
.scope_ancestors(read_scope)
.any(|scope| scope == *binding_scope)
})
&& valtio_path_is_prefix(&target.path, &read_path)
&& valtio_alias_captures_match_target(&alias_captures, &target.path, &read_path)
&& !valtio_target_was_replaced(
target,
read_position,
&proxy_writes,
&alias_captures,
)
});
let Some(matching_target) = matching_target else {
continue;
};
if valtio_is_stable_proxy_dependency(read_expression, &read_path, matching_target, ctx)
{
continue;
}
reported_expression_ids.insert(read_expression.id());
ctx.diagnostic(OxcDiagnostic::warn(MESSAGE).with_label(read_expression.span()));
}
}
}
fn valtio_snapshot_target<'a>(
call_node: &AstNode<'a>,
call: &oxc_ast::ast::CallExpression<'a>,
ctx: &LintContext<'a>,
) -> Option<ValtioSnapshotTarget> {
if !valtio_is_use_snapshot_callee(&call.callee, ctx) {
return None;
}
let proxy_argument = call.arguments.first()?.as_expression()?;
let path =
valtio_resolve_proxy_path_from_expression(proxy_argument, &mut Vec::new(), None, ctx)?;
if path.properties.iter().any(Option::is_none) {
return None;
}
let call_root = transparent_expression_root(call_node, ctx);
let declarator_node = ctx.nodes().parent_node(call_root.id());
let AstKind::VariableDeclarator(declarator) = declarator_node.kind() else {
return None;
};
if declarator
.init
.as_ref()
.is_none_or(|initializer| initializer.span() != call_root.span())
|| !matches!(
ctx.nodes().parent_node(declarator_node.id()).kind(),
AstKind::VariableDeclaration(_)
)
{
return None;
}
let owner_id = find_render_phase_component_or_hook(call_node, ctx)?.id();
let mut snapshot_binding_scopes = Vec::new();
valtio_collect_binding_scopes(&declarator.id, &mut snapshot_binding_scopes, ctx);
(!snapshot_binding_scopes.is_empty()).then_some(ValtioSnapshotTarget {
declaration_end: declarator.span.end,
owner_id,
path,
snapshot_binding_scopes,
})
}
fn valtio_collect_binding_scopes(
pattern: &BindingPattern<'_>,
scopes: &mut Vec<ScopeId>,
ctx: &LintContext<'_>,
) {
match pattern {
BindingPattern::BindingIdentifier(identifier) => {
scopes.push(ctx.scoping().symbol_scope_id(identifier.symbol_id()));
}
BindingPattern::AssignmentPattern(assignment) => {
valtio_collect_binding_scopes(&assignment.left, scopes, ctx);
}
BindingPattern::ArrayPattern(array) => {
for element in array.elements.iter().flatten() {
valtio_collect_binding_scopes(element, scopes, ctx);
}
if let Some(rest) = &array.rest {
valtio_collect_binding_scopes(&rest.argument, scopes, ctx);
}
}
BindingPattern::ObjectPattern(object) => {
for property in &object.properties {
valtio_collect_binding_scopes(&property.value, scopes, ctx);
}
if let Some(rest) = &object.rest {
valtio_collect_binding_scopes(&rest.argument, scopes, ctx);
}
}
}
}
fn valtio_is_use_snapshot_callee<'a>(callee: &Expression<'a>, ctx: &LintContext<'a>) -> bool {
let callee = callee.get_inner_expression();
if let Expression::Identifier(identifier) = callee {
let Some(symbol_id) = valtio_resolve_const_alias_symbol(identifier, ctx) else {
return false;
};
return valtio_import_entry_for_symbol(symbol_id, ctx).is_some_and(|entry| {
matches!(
&entry.import_name,
crate::module_record::ImportImportName::Name(imported_name)
if imported_name.name() == "useSnapshot"
)
});
}
let Some(member) = callee.as_member_expression() else {
return false;
};
if valtio_member_property_name(member).as_deref() != Some("useSnapshot") {
return false;
}
let Expression::Identifier(receiver) = member.object().get_inner_expression() else {
return false;
};
let Some(symbol_id) = valtio_resolve_const_alias_symbol(receiver, ctx) else {
return false;
};
valtio_import_entry_for_symbol(symbol_id, ctx).is_some_and(|entry| {
matches!(
entry.import_name,
crate::module_record::ImportImportName::NamespaceObject
)
})
}
fn valtio_resolve_const_alias_symbol<'a>(
identifier: &oxc_ast::ast::IdentifierReference<'a>,
ctx: &LintContext<'a>,
) -> Option<SymbolId> {
let mut symbol_id = ctx
.scoping()
.get_reference(identifier.reference_id())
.symbol_id()?;
let mut visited_symbol_ids = FxHashSet::default();
loop {
if !visited_symbol_ids.insert(symbol_id) {
return None;
}
let declaration = ctx.symbol_declaration(symbol_id);
let AstKind::VariableDeclarator(declarator) = declaration.kind() else {
return Some(symbol_id);
};
if !matches!(
ctx.nodes().parent_node(declaration.id()).kind(),
AstKind::VariableDeclaration(variable_declaration)
if variable_declaration.kind.is_const()
) || declarator
.id
.get_binding_identifier()
.is_none_or(|binding| binding.symbol_id() != symbol_id)
{
return None;
}
let Some(Expression::Identifier(next_identifier)) = declarator
.init
.as_ref()
.map(Expression::get_inner_expression)
else {
return Some(symbol_id);
};
symbol_id = ctx
.scoping()
.get_reference(next_identifier.reference_id())
.symbol_id()?;
}
}
fn valtio_import_entry_for_symbol<'a>(
symbol_id: SymbolId,
ctx: &'a LintContext<'_>,
) -> Option<&'a crate::module_record::ImportEntry> {
ctx.module_record().import_entries.iter().find(|entry| {
VALTIO_MODULES.contains(&entry.module_request.name())
&& ctx
.scoping()
.get_root_binding(entry.local_name.name().into())
== Some(symbol_id)
})
}
fn valtio_member_property_name(member: &MemberExpression<'_>) -> Option<String> {
match member {
MemberExpression::StaticMemberExpression(member) => Some(member.property.name.to_string()),
MemberExpression::ComputedMemberExpression(member) => {
valtio_computed_member_property_name(member)
}
MemberExpression::PrivateFieldExpression(_) => None,
}
}
fn valtio_computed_member_property_name(
member: &oxc_ast::ast::ComputedMemberExpression<'_>,
) -> Option<String> {
match &member.expression {
Expression::StringLiteral(literal) => Some(literal.value.to_string()),
Expression::TemplateLiteral(template) if template.expressions.is_empty() => {
template.quasis.first().map(|quasi| {
quasi
.value
.cooked
.as_ref()
.map_or(quasi.value.raw.as_str(), |cooked| cooked.as_str())
.to_string()
})
}
_ => None,
}
}
fn valtio_resolve_proxy_path_from_node<'a>(
node: &AstNode<'a>,
visited_symbol_ids: &mut Vec<SymbolId>,
alias_captures: Option<&mut FxHashMap<SymbolId, ValtioProxyAliasCapture>>,
ctx: &LintContext<'a>,
) -> Option<ValtioProxyPath> {
match node.kind() {
AstKind::IdentifierReference(identifier) => valtio_resolve_proxy_path_from_identifier(
identifier,
visited_symbol_ids,
alias_captures,
ctx,
),
AstKind::StaticMemberExpression(member) => valtio_resolve_proxy_member_parts(
&member.object,
Some(member.property.name.as_str()),
visited_symbol_ids,
alias_captures,
ctx,
),
AstKind::ComputedMemberExpression(member) => valtio_resolve_proxy_member_parts(
&member.object,
valtio_computed_member_property_name(member).as_deref(),
visited_symbol_ids,
alias_captures,
ctx,
),
AstKind::PrivateFieldExpression(member) => valtio_resolve_proxy_member_parts(
&member.object,
None,
visited_symbol_ids,
alias_captures,
ctx,
),
AstKind::ParenthesizedExpression(wrapper) => valtio_resolve_proxy_path_from_expression(
&wrapper.expression,
visited_symbol_ids,
alias_captures,
ctx,
),
AstKind::TSAsExpression(wrapper) => valtio_resolve_proxy_path_from_expression(
&wrapper.expression,
visited_symbol_ids,
alias_captures,
ctx,
),
AstKind::TSSatisfiesExpression(wrapper) => valtio_resolve_proxy_path_from_expression(
&wrapper.expression,
visited_symbol_ids,
alias_captures,
ctx,
),
AstKind::TSTypeAssertion(wrapper) => valtio_resolve_proxy_path_from_expression(
&wrapper.expression,
visited_symbol_ids,
alias_captures,
ctx,
),
AstKind::TSInstantiationExpression(wrapper) => valtio_resolve_proxy_path_from_expression(
&wrapper.expression,
visited_symbol_ids,
alias_captures,
ctx,
),
AstKind::TSNonNullExpression(wrapper) => valtio_resolve_proxy_path_from_expression(
&wrapper.expression,
visited_symbol_ids,
alias_captures,
ctx,
),
AstKind::ChainExpression(chain) => match &chain.expression {
ChainElement::TSNonNullExpression(wrapper) => {
valtio_resolve_proxy_path_from_expression(
&wrapper.expression,
visited_symbol_ids,
alias_captures,
ctx,
)
}
chain_element => valtio_resolve_proxy_member_path(
chain_element.as_member_expression()?,
visited_symbol_ids,
alias_captures,
ctx,
),
},
_ => None,
}
}
fn valtio_resolve_proxy_path_from_expression<'a>(
expression: &Expression<'a>,
visited_symbol_ids: &mut Vec<SymbolId>,
alias_captures: Option<&mut FxHashMap<SymbolId, ValtioProxyAliasCapture>>,
ctx: &LintContext<'a>,
) -> Option<ValtioProxyPath> {
let expression = expression.get_inner_expression();
if let Expression::Identifier(identifier) = expression {
return valtio_resolve_proxy_path_from_identifier(
identifier,
visited_symbol_ids,
alias_captures,
ctx,
);
}
valtio_resolve_proxy_member_path(
expression.as_member_expression()?,
visited_symbol_ids,
alias_captures,
ctx,
)
}
fn valtio_resolve_proxy_member_path<'a>(
member: &MemberExpression<'a>,
visited_symbol_ids: &mut Vec<SymbolId>,
alias_captures: Option<&mut FxHashMap<SymbolId, ValtioProxyAliasCapture>>,
ctx: &LintContext<'a>,
) -> Option<ValtioProxyPath> {
valtio_resolve_proxy_member_parts(
member.object(),
valtio_member_property_name(member).as_deref(),
visited_symbol_ids,
alias_captures,
ctx,
)
}
fn valtio_resolve_proxy_member_parts<'a>(
object: &Expression<'a>,
property_name: Option<&str>,
visited_symbol_ids: &mut Vec<SymbolId>,
alias_captures: Option<&mut FxHashMap<SymbolId, ValtioProxyAliasCapture>>,
ctx: &LintContext<'a>,
) -> Option<ValtioProxyPath> {
let mut path =
valtio_resolve_proxy_path_from_expression(object, visited_symbol_ids, alias_captures, ctx)?;
path.properties.push(property_name.map(str::to_string));
Some(path)
}
fn valtio_resolve_proxy_path_from_identifier<'a>(
identifier: &oxc_ast::ast::IdentifierReference<'a>,
visited_symbol_ids: &mut Vec<SymbolId>,
mut alias_captures: Option<&mut FxHashMap<SymbolId, ValtioProxyAliasCapture>>,
ctx: &LintContext<'a>,
) -> Option<ValtioProxyPath> {
let Some(symbol_id) = ctx
.scoping()
.get_reference(identifier.reference_id())
.symbol_id()
else {
return Some(ValtioProxyPath {
root: ValtioProxyRoot::Global(identifier.name.to_string()),
properties: Vec::new(),
});
};
if visited_symbol_ids.contains(&symbol_id) {
return None;
}
let declaration = ctx.symbol_declaration(symbol_id);
let AstKind::VariableDeclarator(declarator) = declaration.kind() else {
return Some(ValtioProxyPath {
root: ValtioProxyRoot::Symbol(symbol_id),
properties: Vec::new(),
});
};
if !matches!(
ctx.nodes().parent_node(declaration.id()).kind(),
AstKind::VariableDeclaration(variable_declaration)
if variable_declaration.kind.is_const()
) {
return Some(ValtioProxyPath {
root: ValtioProxyRoot::Symbol(symbol_id),
properties: Vec::new(),
});
}
let direct_alias = declarator
.id
.get_binding_identifier()
.is_some_and(|binding| binding.symbol_id() == symbol_id);
let pattern_path = (!direct_alias)
.then(|| valtio_pattern_path_to_symbol(&declarator.id, symbol_id))
.flatten();
let Some(initializer) = declarator
.init
.as_ref()
.map(Expression::get_inner_expression)
else {
return Some(ValtioProxyPath {
root: ValtioProxyRoot::Symbol(symbol_id),
properties: Vec::new(),
});
};
let can_resolve_direct_alias = direct_alias
&& (matches!(initializer, Expression::Identifier(_))
|| initializer.as_member_expression().is_some());
if !can_resolve_direct_alias && pattern_path.is_none() {
return Some(ValtioProxyPath {
root: ValtioProxyRoot::Symbol(symbol_id),
properties: Vec::new(),
});
}
visited_symbol_ids.push(symbol_id);
let mut path = valtio_resolve_proxy_path_from_expression(
initializer,
visited_symbol_ids,
alias_captures.as_deref_mut(),
ctx,
)?;
visited_symbol_ids.pop();
path.properties
.extend(pattern_path.unwrap_or_default().into_iter().map(Some));
if let Some(alias_captures) = alias_captures {
alias_captures.insert(
symbol_id,
ValtioProxyAliasCapture {
declaration_end: declarator.span.end,
path: path.clone(),
},
);
}
Some(path)
}
fn valtio_pattern_path_to_symbol(
pattern: &BindingPattern<'_>,
symbol_id: SymbolId,
) -> Option<Vec<String>> {
match pattern {
BindingPattern::BindingIdentifier(identifier) => {
(identifier.symbol_id() == symbol_id).then(Vec::new)
}
BindingPattern::AssignmentPattern(assignment) => {
valtio_pattern_path_to_symbol(&assignment.left, symbol_id)
}
BindingPattern::ArrayPattern(array) => {
for (index, element) in array.elements.iter().enumerate() {
let Some(element) = element else {
continue;
};
if let Some(mut path) = valtio_pattern_path_to_symbol(element, symbol_id) {
path.insert(0, index.to_string());
return Some(path);
}
}
None
}
BindingPattern::ObjectPattern(object) => {
for property in &object.properties {
let Some(mut path) = valtio_pattern_path_to_symbol(&property.value, symbol_id)
else {
continue;
};
if property.computed {
return None;
}
let property_name = match &property.key {
PropertyKey::StaticIdentifier(identifier) => identifier.name.as_str(),
PropertyKey::StringLiteral(literal) => literal.value.as_str(),
_ => return None,
};
path.insert(0, property_name.to_string());
return Some(path);
}
None
}
}
}
fn valtio_outermost_member_read<'a, 'b>(
identifier_node: &'b AstNode<'a>,
ctx: &'b LintContext<'a>,
) -> &'b AstNode<'a> {
let mut current = transparent_expression_root(identifier_node, ctx);
loop {
let parent = ctx.nodes().parent_node(current.id());
let object_span = match parent.kind() {
AstKind::StaticMemberExpression(member) => member.object.span(),
AstKind::ComputedMemberExpression(member) => member.object.span(),
AstKind::PrivateFieldExpression(member) => member.object.span(),
_ => return current,
};
if object_span != current.span() {
return current;
}
current = transparent_expression_root(parent, ctx);
}
}
fn valtio_is_snapshot_argument<'a>(expression: &AstNode<'a>, ctx: &LintContext<'a>) -> bool {
let expression_root = transparent_expression_root(expression, ctx);
let parent = ctx.nodes().parent_node(expression_root.id());
let AstKind::CallExpression(call) = parent.kind() else {
return false;
};
call.arguments
.first()
.and_then(Argument::as_expression)
.is_some_and(|argument| argument.span() == expression_root.span())
&& valtio_is_use_snapshot_callee(&call.callee, ctx)
}
fn valtio_is_stable_proxy_dependency<'a>(
expression: &AstNode<'a>,
read_path: &ValtioProxyPath,
target: &ValtioSnapshotTarget,
ctx: &LintContext<'a>,
) -> bool {
if read_path.properties.len() != target.path.properties.len()
|| !valtio_path_is_prefix(&target.path, read_path)
{
return false;
}
let expression_root = transparent_expression_root(expression, ctx);
let dependency_array_node = ctx.nodes().parent_node(expression_root.id());
let AstKind::ArrayExpression(dependency_array) = dependency_array_node.kind() else {
return false;
};
let hook_call_node = ctx.nodes().parent_node(dependency_array_node.id());
let AstKind::CallExpression(hook_call) = hook_call_node.kind() else {
return false;
};
hook_call
.arguments
.get(1)
.and_then(Argument::as_expression)
.is_some_and(|argument| argument.span() == dependency_array.span)
&& DEPENDENCY_ARRAY_HOOK_NAMES
.iter()
.any(|hook_name| valtio_is_exact_react_api_call(hook_call, hook_name, ctx))
}
fn valtio_is_exact_react_api_call<'a>(
call: &oxc_ast::ast::CallExpression<'a>,
api_name: &str,
ctx: &LintContext<'a>,
) -> bool {
let callee = call.callee.get_inner_expression();
if let Expression::Identifier(identifier) = callee {
let Some(symbol_id) = ctx
.scoping()
.get_reference(identifier.reference_id())
.symbol_id()
else {
return false;
};
return ctx.module_record().import_entries.iter().any(|entry| {
REACT_RUNTIME_MODULES.contains(&entry.module_request.name())
&& ctx
.scoping()
.get_root_binding(entry.local_name.name().into())
== Some(symbol_id)
&& matches!(
&entry.import_name,
crate::module_record::ImportImportName::Name(imported_name)
if imported_name.name() == api_name
)
});
}
let Some(member) = callee.as_member_expression() else {
return false;
};
if valtio_member_property_name(member).as_deref() != Some(api_name) {
return false;
}
let Expression::Identifier(receiver) = member.object().get_inner_expression() else {
return false;
};
if receiver.name == "React"
&& ctx
.scoping()
.get_reference(receiver.reference_id())
.symbol_id()
.is_none()
{
return true;
}
let Some(symbol_id) = valtio_resolve_const_alias_symbol(receiver, ctx) else {
return false;
};
ctx.module_record().import_entries.iter().any(|entry| {
REACT_RUNTIME_MODULES.contains(&entry.module_request.name())
&& ctx
.scoping()
.get_root_binding(entry.local_name.name().into())
== Some(symbol_id)
&& (matches!(
entry.import_name,
crate::module_record::ImportImportName::Default(_)
| crate::module_record::ImportImportName::NamespaceObject
) || matches!(
&entry.import_name,
crate::module_record::ImportImportName::Name(imported_name)
if imported_name.name() == "default"
))
})
}
fn valtio_path_is_prefix(prefix: &ValtioProxyPath, candidate: &ValtioProxyPath) -> bool {
prefix.root == candidate.root
&& prefix.properties.len() <= candidate.properties.len()
&& prefix.properties.iter().zip(&candidate.properties).all(
|(prefix_property, candidate_property)| {
prefix_property.is_some() && prefix_property == candidate_property
},
)
}
fn valtio_alias_captures_match_target(
captures: &FxHashMap<SymbolId, ValtioProxyAliasCapture>,
target_path: &ValtioProxyPath,
read_path: &ValtioProxyPath,
) -> bool {
captures.is_empty()
|| captures.values().any(|capture| {
valtio_path_is_prefix(&capture.path, target_path)
|| (valtio_path_is_prefix(target_path, &capture.path)
&& read_path.properties.len() > capture.path.properties.len())
})
}
fn valtio_target_was_replaced(
target: &ValtioSnapshotTarget,
read_position: u32,
writes: &[ValtioProxyWrite],
captures: &FxHashMap<SymbolId, ValtioProxyAliasCapture>,
) -> bool {
writes.iter().any(|write| {
write.owner_id == target.owner_id
&& write.position > target.declaration_end
&& write.position < read_position
&& valtio_path_is_prefix(&write.path, &target.path)
&& !captures.values().any(|capture| {
capture.declaration_end < write.position
&& valtio_path_is_prefix(&write.path, &capture.path)
})
})
}
fn valtio_collect_assignment_target_writes<'a>(
target: &AssignmentTarget<'a>,
position: u32,
owner_id: NodeId,
writes: &mut Vec<ValtioProxyWrite>,
ctx: &LintContext<'a>,
) {
if let Some(simple_target) = target.as_simple_assignment_target() {
valtio_collect_simple_assignment_target_write(
simple_target,
position,
owner_id,
writes,
ctx,
);
return;
}
match target {
AssignmentTarget::ArrayAssignmentTarget(array) => {
valtio_collect_array_assignment_target_writes(array, position, owner_id, writes, ctx);
}
AssignmentTarget::ObjectAssignmentTarget(object) => {
valtio_collect_object_assignment_target_writes(object, position, owner_id, writes, ctx);
}
_ => {}
}
}
fn valtio_collect_array_assignment_target_writes<'a>(
target: &ArrayAssignmentTarget<'a>,
position: u32,
owner_id: NodeId,
writes: &mut Vec<ValtioProxyWrite>,
ctx: &LintContext<'a>,
) {
for element in target.elements.iter().flatten() {
valtio_collect_assignment_target_maybe_default_writes(
element, position, owner_id, writes, ctx,
);
}
if let Some(rest) = &target.rest {
valtio_collect_assignment_target_writes(&rest.target, position, owner_id, writes, ctx);
}
}
fn valtio_collect_object_assignment_target_writes<'a>(
target: &ObjectAssignmentTarget<'a>,
position: u32,
owner_id: NodeId,
writes: &mut Vec<ValtioProxyWrite>,
ctx: &LintContext<'a>,
) {
for property in &target.properties {
match property {
AssignmentTargetProperty::AssignmentTargetPropertyIdentifier(identifier) => {
if let Some(path) = valtio_resolve_proxy_path_from_identifier(
&identifier.binding,
&mut Vec::new(),
None,
ctx,
) {
writes.push(ValtioProxyWrite {
position,
owner_id,
path,
});
}
}
AssignmentTargetProperty::AssignmentTargetPropertyProperty(property) => {
valtio_collect_assignment_target_maybe_default_writes(
&property.binding,
position,
owner_id,
writes,
ctx,
);
}
}
}
if let Some(rest) = &target.rest {
valtio_collect_assignment_target_writes(&rest.target, position, owner_id, writes, ctx);
}
}
fn valtio_collect_assignment_target_maybe_default_writes<'a>(
target: &AssignmentTargetMaybeDefault<'a>,
position: u32,
owner_id: NodeId,
writes: &mut Vec<ValtioProxyWrite>,
ctx: &LintContext<'a>,
) {
if let Some(simple_target) = target.as_simple_assignment_target() {
valtio_collect_simple_assignment_target_write(
simple_target,
position,
owner_id,
writes,
ctx,
);
return;
}
match target {
AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(defaulted) => {
valtio_collect_assignment_target_writes(
&defaulted.binding,
position,
owner_id,
writes,
ctx,
);
}
AssignmentTargetMaybeDefault::ArrayAssignmentTarget(array) => {
valtio_collect_array_assignment_target_writes(array, position, owner_id, writes, ctx);
}
AssignmentTargetMaybeDefault::ObjectAssignmentTarget(object) => {
valtio_collect_object_assignment_target_writes(object, position, owner_id, writes, ctx);
}
_ => {}
}
}
fn valtio_collect_simple_assignment_target_write<'a>(
target: &SimpleAssignmentTarget<'a>,
position: u32,
owner_id: NodeId,
writes: &mut Vec<ValtioProxyWrite>,
ctx: &LintContext<'a>,
) {
let path = if let SimpleAssignmentTarget::AssignmentTargetIdentifier(identifier) = target {
valtio_resolve_proxy_path_from_identifier(identifier, &mut Vec::new(), None, ctx)
} else if let Some(member) = target.as_member_expression() {
valtio_resolve_proxy_member_path(member, &mut Vec::new(), None, ctx)
} else {
let expression = match target {
SimpleAssignmentTarget::TSAsExpression(wrapper) => Some(&wrapper.expression),
SimpleAssignmentTarget::TSSatisfiesExpression(wrapper) => Some(&wrapper.expression),
SimpleAssignmentTarget::TSNonNullExpression(wrapper) => Some(&wrapper.expression),
SimpleAssignmentTarget::TSTypeAssertion(wrapper) => Some(&wrapper.expression),
_ => None,
};
expression.and_then(|expression| {
valtio_resolve_proxy_path_from_expression(expression, &mut Vec::new(), None, ctx)
})
};
if let Some(path) = path {
writes.push(ValtioProxyWrite {
position,
owner_id,
path,
});
}
}
fn valtio_is_mutation_only_position(expression: &AstNode<'_>, ctx: &LintContext<'_>) -> bool {
let mut current = expression;
for parent in ctx.nodes().ancestors(expression.id()) {
match parent.kind() {
AstKind::ComputedMemberExpression(member)
if member.expression.span() == current.span() =>
{
return false;
}
AstKind::AssignmentTargetPropertyProperty(property)
if property.computed && property.name.span() == current.span() =>
{
return false;
}
AstKind::AssignmentExpression(assignment)
if assignment.left.span().contains_inclusive(expression.span()) =>
{
return true;
}
AstKind::UpdateExpression(update)
if update.argument.span().contains_inclusive(expression.span()) =>
{
return true;
}
AstKind::UnaryExpression(unary)
if unary.operator == UnaryOperator::Delete
&& unary.argument.span().contains_inclusive(expression.span()) =>
{
return true;
}
AstKind::ForInStatement(statement)
if statement.left.span().contains_inclusive(expression.span()) =>
{
return true;
}
AstKind::ForOfStatement(statement)
if statement.left.span().contains_inclusive(expression.span()) =>
{
return true;
}
AstKind::Function(_) | AstKind::ArrowFunctionExpression(_) => return false,
_ => {}
}
current = parent;
}
false
}