mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
1 parent
c97c43c997
commit
128c67d40d
13 files changed
+143
-52
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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 } = {});
|
||||
}
|
||||
@@ -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%)
|
||||
@@ -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%)
|
||||
@@ -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%)
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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%)
|
||||
Reference in new issue
Block a user