Skip to content

Commit 6f0fe38

Browse files
committed
fix(semantic)!: correct all ReferenceFlags::Write according to the spec (#7388)
close #7323 According to the specification re-design the JavaScript-part ReferenceFlags inferring approach. * https://tc39.es/ecma262/#sec-assignment-operators-runtime-semantics-evaluation * https://tc39.es/ecma262/#sec-postfix-increment-operator-runtime-semantics-evaluation * https://tc39.es/ecma262/#sec-runtime-semantics-restdestructuringassignmentevaluation * ... See references of https://tc39.es/ecma262/#sec-putvalue ### Changes 1. The left-hand of `AssignmentExpression` is always `ReferenceFlags::Write` ```js let a = 0; console.log(a = 0); ^ Write only ``` 2. The `argument` of `UpdateExpression` is always `ReferenceFlags::Read | Write` ```js let a = 0; a++; ^ Read and Write ``` This change might cause some trouble for `Minfier` to remove this code, because ‘a’ is not used elsewhere. I have taken a look at `esbuild` and `Terser`. Only the `Terser` can remove this code.
1 parent 6730e3e commit 6f0fe38

File tree

25 files changed

+17672
-201
lines changed

25 files changed

+17672
-201
lines changed

crates/oxc_linter/src/rules/eslint/no_const_assign.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ fn test() {
8282
("try {} catch (x) { x = 1; }", None),
8383
("const a = 1; { let a = 2; { a += 1; } }", None),
8484
("const foo = 1;let bar;bar[foo ?? foo] = 42;", None),
85+
("const FOO = 1; ({ files = FOO } = arg1); ", None),
8586
];
8687

8788
let fail = vec![

crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/eslint.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ fn test() {
3434
let pass = vec![
3535
(
3636
"var foo = 5;
37-
37+
3838
label: while (true) {
3939
console.log(foo);
4040
break label;
@@ -43,7 +43,7 @@ fn test() {
4343
),
4444
(
4545
"var foo = 5;
46-
46+
4747
while (true) {
4848
console.log(foo);
4949
break;

crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,6 @@ fn test_vars_reassignment() {
270270
}
271271
",
272272
"let a = 0; let b = a++; f(b);",
273-
"let a = 0, b = 1; let c = b = a = 1; f(c+b);",
274273
// implicit returns
275274
"
276275
let i = 0;
@@ -332,6 +331,7 @@ fn test_vars_reassignment() {
332331
"let a = 0; let b = (0, (a++, 0)); f(b);",
333332
"let a = 0; let b = ((0, a++), 0); f(b);",
334333
"let a = 0; let b = (a, 0) + 1; f(b);",
334+
"let a = 0, b = 1; let c = b = a = 1; f(c+b);",
335335
];
336336

337337
Tester::new(NoUnusedVars::NAME, pass, fail)

crates/oxc_linter/src/snapshots/no_unused_vars@eslint.snap

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
---
22
source: crates/oxc_linter/src/tester.rs
3-
snapshot_kind: text
43
---
54
eslint(no-unused-vars): Function 'foox' is declared but never used.
65
╭─[no_unused_vars.tsx:1:10]
@@ -261,6 +260,15 @@ snapshot_kind: text
261260
╰────
262261
help: Consider removing this declaration.
263262

263+
eslint(no-unused-vars): Variable 'a' is assigned a value but never used.
264+
╭─[no_unused_vars.tsx:1:20]
265+
1function f() { var a = 1; return function(){ f(a = 2); }; }
266+
· ┬ ┬
267+
· │ ╰── it was last assigned here
268+
· ╰── 'a' is declared here
269+
╰────
270+
help: Did you mean to use this variable?
271+
264272
eslint(no-unused-vars): Identifier 'x' is imported but never used.
265273
╭─[no_unused_vars.tsx:1:8]
266274
1import x from "y";

crates/oxc_linter/src/snapshots/no_unused_vars@oxc-vars-reassignment.snap

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
---
22
source: crates/oxc_linter/src/tester.rs
3-
snapshot_kind: text
43
---
54
eslint(no-unused-vars): Variable 'a' is assigned a value but never used. Unused variables should start with a '_'.
65
╭─[no_unused_vars.tsx:1:5]
@@ -99,3 +98,12 @@ snapshot_kind: text
9998
· ╰── 'a' is declared here
10099
╰────
101100
help: Consider removing this declaration.
101+
102+
eslint(no-unused-vars): Variable 'a' is assigned a value but never used. Unused variables should start with a '_'.
103+
╭─[no_unused_vars.tsx:1:5]
104+
1let a = 0, b = 1; let c = b = a = 1; f(c+b);
105+
· ┬ ┬
106+
· │ ╰── it was last assigned here
107+
· ╰── 'a' is declared here
108+
╰────
109+
help: Did you mean to use this variable?

crates/oxc_semantic/src/builder.rs

Lines changed: 92 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use oxc_cfg::{
1515
};
1616
use oxc_diagnostics::OxcDiagnostic;
1717
use oxc_span::{Atom, CompactStr, SourceType, Span};
18-
use oxc_syntax::{module_record::ModuleRecord, operator::AssignmentOperator};
18+
use oxc_syntax::module_record::ModuleRecord;
1919

2020
use crate::{
2121
binder::Binder,
@@ -890,8 +890,17 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> {
890890

891891
let kind = AstKind::AssignmentExpression(self.alloc(expr));
892892
self.enter_node(kind);
893+
894+
if !expr.operator.is_assign() {
895+
// Only when the operator is not an `=` operator, the left-hand side is both read and write.
896+
// <https://tc39.es/ecma262/#sec-assignment-operators-runtime-semantics-evaluation>
897+
self.current_reference_flags = ReferenceFlags::read_write();
898+
}
899+
893900
self.visit_assignment_target(&expr.left);
894901

902+
self.current_reference_flags = ReferenceFlags::empty();
903+
895904
/* cfg */
896905
let cfg_ixs = control_flow!(self, |cfg| {
897906
if expr.operator.is_logical() {
@@ -1760,6 +1769,86 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> {
17601769
self.leave_node(kind);
17611770
self.leave_scope();
17621771
}
1772+
1773+
fn visit_update_expression(&mut self, it: &UpdateExpression<'a>) {
1774+
let kind = AstKind::UpdateExpression(self.alloc(it));
1775+
self.enter_node(kind);
1776+
// `++a` or `a--`
1777+
// ^ ^ We always treat `a` as Read and Write reference,
1778+
self.current_reference_flags = ReferenceFlags::read_write();
1779+
self.visit_simple_assignment_target(&it.argument);
1780+
self.current_reference_flags = ReferenceFlags::empty();
1781+
self.leave_node(kind);
1782+
}
1783+
1784+
fn visit_member_expression(&mut self, it: &MemberExpression<'a>) {
1785+
let kind = AstKind::MemberExpression(self.alloc(it));
1786+
self.enter_node(kind);
1787+
1788+
// A.B = 1;
1789+
// ^^^ Can't treat A as a Write reference since it's A's property(B) that changes.
1790+
self.current_reference_flags -= ReferenceFlags::Write;
1791+
1792+
match it {
1793+
MemberExpression::ComputedMemberExpression(it) => {
1794+
self.visit_computed_member_expression(it);
1795+
}
1796+
MemberExpression::StaticMemberExpression(it) => self.visit_static_member_expression(it),
1797+
MemberExpression::PrivateFieldExpression(it) => self.visit_private_field_expression(it),
1798+
}
1799+
self.leave_node(kind);
1800+
}
1801+
1802+
fn visit_simple_assignment_target(&mut self, it: &SimpleAssignmentTarget<'a>) {
1803+
let kind = AstKind::SimpleAssignmentTarget(self.alloc(it));
1804+
self.enter_node(kind);
1805+
let prev_reference_flags = self.current_reference_flags;
1806+
// Except that the read-write flags has been set in visit_assignment_expression
1807+
// and visit_update_expression, this is always a write-only reference here.
1808+
if !self.current_reference_flags.is_write() {
1809+
self.current_reference_flags = ReferenceFlags::Write;
1810+
}
1811+
1812+
match it {
1813+
SimpleAssignmentTarget::AssignmentTargetIdentifier(it) => {
1814+
self.visit_identifier_reference(it);
1815+
}
1816+
SimpleAssignmentTarget::TSAsExpression(it) => {
1817+
self.visit_ts_as_expression(it);
1818+
}
1819+
SimpleAssignmentTarget::TSSatisfiesExpression(it) => {
1820+
self.visit_ts_satisfies_expression(it);
1821+
}
1822+
SimpleAssignmentTarget::TSNonNullExpression(it) => {
1823+
self.visit_ts_non_null_expression(it);
1824+
}
1825+
SimpleAssignmentTarget::TSTypeAssertion(it) => {
1826+
self.visit_ts_type_assertion(it);
1827+
}
1828+
SimpleAssignmentTarget::TSInstantiationExpression(it) => {
1829+
self.visit_ts_instantiation_expression(it);
1830+
}
1831+
match_member_expression!(SimpleAssignmentTarget) => {
1832+
self.visit_member_expression(it.to_member_expression());
1833+
}
1834+
}
1835+
self.current_reference_flags = prev_reference_flags;
1836+
self.leave_node(kind);
1837+
}
1838+
1839+
fn visit_assignment_target_property_identifier(
1840+
&mut self,
1841+
it: &AssignmentTargetPropertyIdentifier<'a>,
1842+
) {
1843+
// NOTE: AstKind doesn't exists!
1844+
let prev_reference_flags = self.current_reference_flags;
1845+
self.current_reference_flags = ReferenceFlags::Write;
1846+
self.visit_identifier_reference(&it.binding);
1847+
self.current_reference_flags = prev_reference_flags;
1848+
if let Some(init) = &it.init {
1849+
self.visit_expression(init);
1850+
}
1851+
}
17631852
}
17641853

17651854
impl<'a> SemanticBuilder<'a> {
@@ -1940,29 +2029,6 @@ impl<'a> SemanticBuilder<'a> {
19402029
AstKind::IdentifierReference(ident) => {
19412030
self.reference_identifier(ident);
19422031
}
1943-
AstKind::UpdateExpression(_) => {
1944-
if !self.current_reference_flags.is_type()
1945-
&& self.is_not_expression_statement_parent()
1946-
{
1947-
self.current_reference_flags |= ReferenceFlags::Read;
1948-
}
1949-
self.current_reference_flags |= ReferenceFlags::Write;
1950-
}
1951-
AstKind::AssignmentExpression(expr) => {
1952-
if expr.operator != AssignmentOperator::Assign
1953-
|| self.is_not_expression_statement_parent()
1954-
{
1955-
self.current_reference_flags |= ReferenceFlags::Read;
1956-
}
1957-
}
1958-
AstKind::MemberExpression(_) => {
1959-
// A.B = 1;
1960-
// ^^^ we can't treat A as Write reference, because it's the property(B) of A that change
1961-
self.current_reference_flags -= ReferenceFlags::Write;
1962-
}
1963-
AstKind::AssignmentTarget(_) => {
1964-
self.current_reference_flags |= ReferenceFlags::Write;
1965-
}
19662032
AstKind::LabeledStatement(stmt) => {
19672033
self.unused_labels.add(stmt.label.name.as_str());
19682034
}
@@ -2018,27 +2084,12 @@ impl<'a> SemanticBuilder<'a> {
20182084
AstKind::TSTypeName(_) => {
20192085
self.current_reference_flags -= ReferenceFlags::Type;
20202086
}
2021-
AstKind::UpdateExpression(_) => {
2022-
if self.is_not_expression_statement_parent() {
2023-
self.current_reference_flags -= ReferenceFlags::Read;
2024-
}
2025-
self.current_reference_flags -= ReferenceFlags::Write;
2026-
}
2027-
AstKind::AssignmentExpression(_) | AstKind::ExportNamedDeclaration(_)
2087+
AstKind::ExportNamedDeclaration(_)
20282088
| AstKind::TSTypeQuery(_)
20292089
// Clear the reference flags that are set in AstKind::PropertySignature
20302090
| AstKind::PropertyKey(_) => {
20312091
self.current_reference_flags = ReferenceFlags::empty();
20322092
}
2033-
AstKind::AssignmentTarget(_) =>{
2034-
// Handle nested assignment targets like `({a: b} = obj)`
2035-
if !matches!(
2036-
self.nodes.parent_kind(self.current_node_id),
2037-
Some(AstKind::ObjectAssignmentTarget(_) | AstKind::ArrayAssignmentTarget(_))
2038-
) {
2039-
self.current_reference_flags -= ReferenceFlags::Write;
2040-
}
2041-
},
20422093
AstKind::LabeledStatement(_) => self.unused_labels.mark_unused(self.current_node_id),
20432094
_ => {}
20442095
}
@@ -2066,34 +2117,12 @@ impl<'a> SemanticBuilder<'a> {
20662117
}
20672118

20682119
/// Resolve reference flags for the current ast node.
2120+
#[inline]
20692121
fn resolve_reference_usages(&self) -> ReferenceFlags {
20702122
if self.current_reference_flags.is_empty() {
20712123
ReferenceFlags::Read
20722124
} else {
20732125
self.current_reference_flags
20742126
}
20752127
}
2076-
2077-
fn is_not_expression_statement_parent(&self) -> bool {
2078-
for node in self.nodes.ancestors(self.current_node_id).skip(1) {
2079-
return match node.kind() {
2080-
AstKind::ParenthesizedExpression(_) => continue,
2081-
AstKind::ExpressionStatement(_) => {
2082-
if self.current_scope_flags().is_arrow() {
2083-
if let Some(node) = self.nodes.ancestors(node.id()).nth(2) {
2084-
// (x) => x++
2085-
// ^^^ implicit return, we need to treat `x` as a read reference
2086-
if matches!(node.kind(), AstKind::ArrowFunctionExpression(arrow) if arrow.expression)
2087-
{
2088-
return true;
2089-
}
2090-
}
2091-
}
2092-
false
2093-
}
2094-
_ => true,
2095-
};
2096-
}
2097-
false
2098-
}
20992128
}

crates/oxc_semantic/src/lib.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -365,9 +365,8 @@ mod tests {
365365
// assignment expressions count as read-write
366366
(SourceType::default(), "let a = 1, b; b = a += 5", ReferenceFlags::read_write()),
367367
(SourceType::default(), "let a = 1; a += 5", ReferenceFlags::read_write()),
368-
// note: we consider a to be written, and the read of `1` propagates upwards
369-
(SourceType::default(), "let a, b; b = a = 1", ReferenceFlags::read_write()),
370-
(SourceType::default(), "let a, b; b = (a = 1)", ReferenceFlags::read_write()),
368+
(SourceType::default(), "let a, b; b = a = 1", ReferenceFlags::write()),
369+
(SourceType::default(), "let a, b; b = (a = 1)", ReferenceFlags::write()),
371370
(SourceType::default(), "let a, b, c; b = c = a", ReferenceFlags::read()),
372371
// sequences return last read_write in sequence
373372
(SourceType::default(), "let a, b; b = (0, a++)", ReferenceFlags::read_write()),
@@ -404,27 +403,28 @@ mod tests {
404403
// least, or now)
405404
(SourceType::default(), "let a, b; b = (a, 0)", ReferenceFlags::read()),
406405
(SourceType::default(), "let a, b; b = (--a, 0)", ReferenceFlags::read_write()),
407-
// other reads after a is written
408-
// a = 1 writes, but the CallExpression reads the rhs (1) so a isn't read
409406
(
410407
SourceType::default(),
411408
"let a; function foo(a) { return a }; foo(a = 1)",
412-
ReferenceFlags::read_write(),
409+
// ^ write
410+
ReferenceFlags::write(),
413411
),
414412
// member expression
415413
(SourceType::default(), "let a; a.b = 1", ReferenceFlags::read()),
416414
(SourceType::default(), "let a; let b; b[a += 1] = 1", ReferenceFlags::read_write()),
417415
(
418416
SourceType::default(),
419417
"let a; let b; let c; b[c[a = c['a']] = 'c'] = 'b'",
420-
ReferenceFlags::read_write(),
418+
// ^ write
419+
ReferenceFlags::write(),
421420
),
422421
(
423422
SourceType::default(),
424423
"let a; let b; let c; a[c[b = c['a']] = 'c'] = 'b'",
425424
ReferenceFlags::read(),
426425
),
427-
(SourceType::default(), "console.log;let a=0;a++", ReferenceFlags::write()),
426+
(SourceType::default(), "console.log;let a=0;a++", ReferenceFlags::read_write()),
427+
// ^^^ UpdateExpression is always a read | write
428428
// typescript
429429
(typescript, "let a: number = 1; (a as any) = true", ReferenceFlags::write()),
430430
(typescript, "let a: number = 1; a = true as any", ReferenceFlags::write()),
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
let read = {}, write = {};
2+
3+
[write = read, [write = read]] = ref;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
source: crates/oxc_semantic/tests/main.rs
3+
input_file: crates/oxc_semantic/tests/fixtures/oxc/assignment/array.js
4+
---
5+
[
6+
{
7+
"children": [],
8+
"flags": "ScopeFlags(StrictMode | Top)",
9+
"id": 0,
10+
"node": "Program",
11+
"symbols": [
12+
{
13+
"flags": "SymbolFlags(BlockScopedVariable)",
14+
"id": 0,
15+
"name": "read",
16+
"node": "VariableDeclarator(read)",
17+
"references": [
18+
{
19+
"flags": "ReferenceFlags(Read)",
20+
"id": 1,
21+
"name": "read",
22+
"node_id": 17
23+
},
24+
{
25+
"flags": "ReferenceFlags(Read)",
26+
"id": 3,
27+
"name": "read",
28+
"node_id": 25
29+
}
30+
]
31+
},
32+
{
33+
"flags": "SymbolFlags(BlockScopedVariable)",
34+
"id": 1,
35+
"name": "write",
36+
"node": "VariableDeclarator(write)",
37+
"references": [
38+
{
39+
"flags": "ReferenceFlags(Write)",
40+
"id": 0,
41+
"name": "write",
42+
"node_id": 16
43+
},
44+
{
45+
"flags": "ReferenceFlags(Write)",
46+
"id": 2,
47+
"name": "write",
48+
"node_id": 24
49+
}
50+
]
51+
}
52+
]
53+
}
54+
]

0 commit comments

Comments
 (0)