mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-17 12:56:09 +00:00
fix(semantic): mark delete of optional members as MemberWriteTarget (#27430)
Fixes #27419 `delete a.b` marks `a` as `Read | MemberWriteTarget`. `delete a?.b` and `delete (a.b)` did not, because `visit_unary_expression` only set the write context when `argument.is_member_expression()`. An optional chain is a `ChainExpression`, and parentheses wrap the member, so the check failed and `visit_member_expression` never saw a write. Unwrap parentheses/type assertions, and treat a chain whose element is a member (not a call) as a delete target. Bare `delete x` is unchanged. Rolldown reads `MemberWriteTarget` to detect writes through member chains (rolldown/rolldown#11146).
This commit is contained in:
1 parent
408d87f628
commit
558a8908f2
2 files changed
+53
-2
No files matched your search
@@ -2239,7 +2239,10 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> {
|
||||
// Set Write so `visit_member_expression` can detect it and mark MemberWriteTarget.
|
||||
// Only for member expressions — `delete x` (bare identifier in sloppy mode)
|
||||
// is not a property modification.
|
||||
if it.operator == UnaryOperator::Delete && it.argument.is_member_expression() {
|
||||
// `is_member_expression` misses `delete a?.b` (a `ChainExpression`) and
|
||||
// `delete (a.b)` (a `ParenthesizedExpression`). Unwrap those so the
|
||||
// member still sees the write context.
|
||||
if it.operator == UnaryOperator::Delete && is_delete_member_target(&it.argument) {
|
||||
self.current_reference_flags = ReferenceFlags::Write;
|
||||
}
|
||||
self.visit_expression(&it.argument);
|
||||
@@ -2933,6 +2936,23 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `delete` only modifies a property when its argument is a member expression.
|
||||
///
|
||||
/// Optional chains (`delete a?.b`) are `ChainExpression`, and parentheses or
|
||||
/// type assertions wrap the member (`delete (a.b)`, `delete (a as T).b`).
|
||||
/// Bare `delete x` is not a property modification.
|
||||
fn is_delete_member_target(expr: &Expression<'_>) -> bool {
|
||||
match expr.get_inner_expression() {
|
||||
Expression::ChainExpression(chain) => match &chain.expression {
|
||||
ChainElement::StaticMemberExpression(_)
|
||||
| ChainElement::ComputedMemberExpression(_)
|
||||
| ChainElement::PrivateFieldExpression(_) => true,
|
||||
ChainElement::CallExpression(_) | ChainElement::TSNonNullExpression(_) => false,
|
||||
},
|
||||
inner => inner.is_member_expression(),
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SemanticBuilder<'a> {
|
||||
#[inline]
|
||||
fn reference_identifier(&mut self, ident: &IdentifierReference<'a>) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use oxc_semantic::{Reference, SymbolFlags};
|
||||
use oxc_semantic::{Reference, ReferenceFlags, SymbolFlags};
|
||||
|
||||
use crate::util::SemanticTester;
|
||||
|
||||
@@ -25,6 +25,37 @@ fn assert_root_and_single_local_reference_counts(
|
||||
}
|
||||
}
|
||||
|
||||
/// `delete a?.b` and `delete (a.b)` are property writes, same as `delete a.b`.
|
||||
/// https://github.com/oxc-project/oxc/issues/27419
|
||||
#[test]
|
||||
fn test_delete_member_write_target() {
|
||||
for code in [
|
||||
"delete a.b",
|
||||
"delete a.b.c",
|
||||
"delete a?.b",
|
||||
"delete a?.b.c",
|
||||
"delete a.b?.c",
|
||||
"delete (a.b)",
|
||||
] {
|
||||
let source = format!("const a = {{}}; {code};");
|
||||
let source: &'static str = Box::leak(source.into_boxed_str());
|
||||
let tester = SemanticTester::js(source);
|
||||
let semantic = tester.build();
|
||||
let scoping = semantic.scoping();
|
||||
let symbol = scoping.get_root_binding("a".into()).unwrap();
|
||||
let flags: Vec<_> =
|
||||
scoping.get_resolved_references(symbol).map(oxc_semantic::Reference::flags).collect();
|
||||
assert_eq!(flags, vec![ReferenceFlags::Read | ReferenceFlags::MemberWriteTarget], "{code}");
|
||||
}
|
||||
|
||||
// Bare `delete` is not a property modification.
|
||||
SemanticTester::js("let x; delete x;")
|
||||
.with_script(true)
|
||||
.has_some_symbol("x")
|
||||
.has_number_of_references_where(0, |reference| reference.flags().is_member_write_target())
|
||||
.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_class_simple() {
|
||||
SemanticTester::js("export class Foo {};")
|
||||
|
||||
Reference in new issue
Block a user