fix(semantic): check strict eval and arguments writes by reference flags (#28445)

Use reference write flags to reject strict-mode assignments to `eval`
and `arguments`, including for-in/of, while accepting reads in
destructuring keys and defaults. Preserve those flags across TypeScript
assertion annotations.

Validated scoped Rust checks, semantic conformance, and
reference/diagnostic regressions.

AI-assisted (Codex).
This commit is contained in:
Boshen authored and GitHub committed 2026-10-30 17:17:30 +08:00
1 parent c97c43c997
commit 128c67d40d
13 files changed
+143 -52

No files matched your search

+12
View File
@@ -2919,6 +2919,18 @@ impl<'a> Visit<'a> for SemanticBuilder<'a> {
self.leave_node(kind);
}
fn visit_ts_type_assertion(&mut self, it: &TSTypeAssertion<'a>) {
let kind = AstKind::TSTypeAssertion(self.alloc(it));
self.enter_node(kind);
self.visit_span(&it.span);
// The type annotation must not consume the asserted expression's reference flags.
let reference_flags = mem::take(&mut self.current_reference_flags);
self.visit_ts_type(&it.type_annotation);
self.current_reference_flags = reference_flags;
self.visit_expression(&it.expression);
self.leave_node(kind);
}
fn visit_ts_type_reference(&mut self, ty: &TSTypeReference<'a>) {
let kind = AstKind::TSTypeReference(self.alloc(ty));
self.enter_node(kind);
+11 -33
View File
@@ -293,39 +293,17 @@ pub fn check_identifier_reference(ident: &IdentifierReference, ctx: &SemanticBui
// Static Semantics: AssignmentTargetType
// 1. If this IdentifierReference is contained in strict mode code and StringValue of Identifier is "eval" or "arguments", return invalid.
if matches!(ident.name.as_str(), "arguments" | "eval") && ctx.strict_mode() {
for node_kind in ctx.ancestry().ancestor_kinds() {
match node_kind {
// Only check for actual assignment contexts, not member expression access
AstKind::ObjectAssignmentTarget(_)
| AstKind::AssignmentTargetPropertyIdentifier(_)
| AstKind::UpdateExpression(_)
| AstKind::ArrayAssignmentTarget(_) => {
return ctx.error(diagnostics::unexpected_identifier_assign(
&ident.name,
ident.span,
unexpected_identifier_assign_context(ctx),
));
}
AstKind::AssignmentExpression(assign_expr) => {
// only throw error if arguments or eval are being assigned to
if let AssignmentTarget::AssignmentTargetIdentifier(target_ident) =
&assign_expr.left
&& target_ident.name == ident.name
{
return ctx.error(diagnostics::unexpected_identifier_assign(
&ident.name,
ident.span,
unexpected_identifier_assign_context(ctx),
));
}
}
m if m.is_member_expression_kind() => {
break;
}
_ => {}
}
}
if matches!(ident.name.as_str(), "arguments" | "eval")
&& ctx.strict_mode()
// Reference flags distinguish assignment targets (including for-in/of) from
// reads in computed keys and default initializers within the same pattern.
&& ctx.scoping.get_reference(ident.reference_id.get().unwrap()).flags().is_write()
{
return ctx.error(diagnostics::unexpected_identifier_assign(
&ident.name,
ident.span,
unexpected_identifier_assign_context(ctx),
));
}
// FieldDefinition : ClassElementName Initializeropt
@@ -1,4 +1,5 @@
use oxc_semantic::{Reference, ReferenceFlags, SymbolFlags};
use oxc_span::SourceType;
use crate::util::SemanticTester;
@@ -56,6 +57,80 @@ fn test_delete_member_write_target() {
.test();
}
#[test]
fn test_type_assertion_reference_flags() {
for (annotation, type_references) in [
("any", &[][..]),
("T", &["T"][..]),
("T[]", &["T"][..]),
("Box<T>", &["Box", "T"][..]),
("typeof typeValue", &["typeValue"][..]),
] {
for (expression, expected_flags) in [
(format!("[<{annotation}>value] = []"), ReferenceFlags::Write),
(format!("({{ key: <{annotation}>value }} = {{}})"), ReferenceFlags::Write),
(format!("(<{annotation}>value) += 1"), ReferenceFlags::read_write()),
(format!("(<{annotation}>value)++"), ReferenceFlags::read_write()),
(format!("for (<{annotation}>value of []) {{}}"), ReferenceFlags::Write),
(
format!("(<{annotation}>value).key = 1"),
ReferenceFlags::Read | ReferenceFlags::MemberWriteTarget,
),
(format!("<{annotation}>value"), ReferenceFlags::Read),
(format!("[other = <{annotation}>value] = []"), ReferenceFlags::Read),
(format!("({{ [<{annotation}>value]: other }} = {{}})"), ReferenceFlags::Read),
] {
let source = format!(
"type T = any; type Box<V> = V; let typeValue, value, other; {expression};"
);
let tester = SemanticTester::new(&source, SourceType::ts());
let semantic = tester.build();
let scoping = semantic.scoping();
for (name, expected) in std::iter::once(("value", expected_flags))
.chain(type_references.iter().map(|&name| (name, ReferenceFlags::Type)))
{
let symbol = scoping.get_root_binding(name.into()).unwrap();
let flags: Vec<_> =
scoping.get_resolved_references(symbol).map(Reference::flags).collect();
assert_eq!(flags, vec![expected], "{source}: {name}");
}
}
}
}
#[test]
fn test_strict_type_assertion_assignments() {
for name in ["eval", "arguments"] {
for annotation in ["any", "T", "T[]", "typeof eval"] {
for (expression, is_write) in [
(format!("[<{annotation}>{name}] = []"), true),
(format!("({{ value: <{annotation}>{name} }} = {{}})"), true),
(format!("[...<{annotation}>{name}] = []"), true),
(format!("[<{annotation}>{name} = value] = []"), true),
(format!("[<{annotation}><{annotation}>{name}] = []"), true),
(format!("for (<{annotation}>{name} of []) {{}}"), true),
(format!("(<{annotation}>{name})++"), true),
(format!("<{annotation}>{name}"), false),
(format!("[value = <{annotation}>{name}] = []"), false),
(format!("({{ [<{annotation}>{name}]: value }} = {{}})"), false),
(format!("(<{annotation}>{name}).prop = 1"), false),
(format!("[<{annotation}>{name}.prop] = []"), false),
] {
let source = format!("\"use strict\"; type T = any; let value; {expression};");
let tester = SemanticTester::new(&source, SourceType::ts()).with_script(true);
let result = tester.build_with_errors();
let messages: Vec<_> = result.diagnostics.iter().map(ToString::to_string).collect();
let expected = if is_write {
vec![format!("Cannot assign to '{name}' in strict mode")]
} else {
vec![]
};
assert_eq!(messages, expected, "{source}");
}
}
}
}
#[test]
fn test_class_simple() {
SemanticTester::js("export class Foo {};")
@@ -0,0 +1,2 @@
"use strict";
for (arguments of []) {}
@@ -0,0 +1,2 @@
"use strict";
for (eval in {}) {}
@@ -0,0 +1,11 @@
"use strict";
let value;
[value = eval] = [];
[value = () => eval] = [];
({ value = eval } = {});
({ [eval]: value } = {});
function readArguments() {
[value = arguments] = [];
({ value = arguments } = {});
({ [arguments]: value } = {});
}
+2 -2
View File
@@ -1,3 +1,3 @@
codegen_misc Summary:
AST Parsed : 130/130 (100.00%)
Positive Passed: 130/130 (100.00%)
AST Parsed : 131/131 (100.00%)
Positive Passed: 131/131 (100.00%)
+2 -2
View File
@@ -1,3 +1,3 @@
formatter_misc Summary:
AST Parsed : 130/130 (100.00%)
Positive Passed: 130/130 (100.00%)
AST Parsed : 131/131 (100.00%)
Positive Passed: 131/131 (100.00%)
+2 -2
View File
@@ -1,3 +1,3 @@
lexer_misc Summary:
AST Parsed : 395/395 (100.00%)
Positive Passed: 395/395 (100.00%)
AST Parsed : 398/398 (100.00%)
Positive Passed: 398/398 (100.00%)
+19 -3
View File
@@ -1,7 +1,7 @@
parser_misc Summary:
AST Parsed : 130/130 (100.00%)
Positive Passed: 130/130 (100.00%)
Negative Passed: 267/267 (100.00%)
AST Parsed : 131/131 (100.00%)
Positive Passed: 131/131 (100.00%)
Negative Passed: 269/269 (100.00%)
× TS(1275): 'accessor' modifier can only appear on a property declaration.
╭─[misc/fail/accessor-modifier-abstract-method.ts:1:29]
@@ -5211,6 +5211,22 @@ Negative Passed: 267/267 (100.00%)
5 │ }
╰────
× Cannot assign to 'arguments' in strict mode
╭─[misc/fail/strict-arguments-for-of.js:2:6]
1 │ "use strict";
2 │ for (arguments of []) {}
· ─────────
╰────
note: This is strict mode code
× Cannot assign to 'eval' in strict mode
╭─[misc/fail/strict-eval-for-in.js:2:6]
1 │ "use strict";
2 │ for (eval in {}) {}
· ────
╰────
note: This is strict mode code
× Expected switch clause
╭─[misc/fail/switch-invalid-clause.js:2:3]
1 │ switch (foo) {
+1 -6
View File
@@ -2,7 +2,7 @@ commit: b835f0c6
semantic_babel Summary:
AST Parsed : 2304/2304 (100.00%)
Positive Passed: 2209/2304 (95.88%)
Positive Passed: 2210/2304 (95.92%)
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/annex-b/enabled/1.1-html-comments-open-export-default-from/input.js
Cannot use export statement outside a module
@@ -85,11 +85,6 @@ Bindings mismatch:
after transform: ScopeId(0): ["assertIsString"]
rebuilt : ScopeId(0): []
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/cast/for-of-lhs/input.ts
Reference flags mismatch for "a":
after transform: ReferenceId(3): ReferenceFlags(Read)
rebuilt : ReferenceId(1): ReferenceFlags(Write)
semantic Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/typescript/class/abstract/input.ts
Bindings mismatch:
after transform: ScopeId(0): ["C1", "C2", "C3", "C4", "C5"]
+2 -2
View File
@@ -1,6 +1,6 @@
semantic_misc Summary:
AST Parsed : 130/130 (100.00%)
Positive Passed: 124/130 (95.38%)
AST Parsed : 131/131 (100.00%)
Positive Passed: 125/131 (95.42%)
semantic Error: tasks/coverage/misc/pass/declare-let-private.ts
Bindings mismatch:
after transform: ScopeId(0): ["private"]
@@ -1,3 +1,3 @@
transformer_misc Summary:
AST Parsed : 130/130 (100.00%)
Positive Passed: 130/130 (100.00%)
AST Parsed : 131/131 (100.00%)
Positive Passed: 131/131 (100.00%)