diff --git a/crates/oxc_semantic/src/builder.rs b/crates/oxc_semantic/src/builder.rs index 585fe3dd61..2ffd241792 100644 --- a/crates/oxc_semantic/src/builder.rs +++ b/crates/oxc_semantic/src/builder.rs @@ -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); diff --git a/crates/oxc_semantic/src/checker/javascript.rs b/crates/oxc_semantic/src/checker/javascript.rs index 92f383d060..cd3468dd74 100644 --- a/crates/oxc_semantic/src/checker/javascript.rs +++ b/crates/oxc_semantic/src/checker/javascript.rs @@ -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 diff --git a/crates/oxc_semantic/tests/integration/symbols.rs b/crates/oxc_semantic/tests/integration/symbols.rs index 3059662dee..bb1dda8753 100644 --- a/crates/oxc_semantic/tests/integration/symbols.rs +++ b/crates/oxc_semantic/tests/integration/symbols.rs @@ -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", &["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; 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 {};") diff --git a/tasks/coverage/misc/fail/strict-arguments-for-of.js b/tasks/coverage/misc/fail/strict-arguments-for-of.js new file mode 100644 index 0000000000..b457ffc9cd --- /dev/null +++ b/tasks/coverage/misc/fail/strict-arguments-for-of.js @@ -0,0 +1,2 @@ +"use strict"; +for (arguments of []) {} diff --git a/tasks/coverage/misc/fail/strict-eval-for-in.js b/tasks/coverage/misc/fail/strict-eval-for-in.js new file mode 100644 index 0000000000..5071d5e18d --- /dev/null +++ b/tasks/coverage/misc/fail/strict-eval-for-in.js @@ -0,0 +1,2 @@ +"use strict"; +for (eval in {}) {} diff --git a/tasks/coverage/misc/pass/destructuring-eval-arguments-reads.js b/tasks/coverage/misc/pass/destructuring-eval-arguments-reads.js new file mode 100644 index 0000000000..c011241109 --- /dev/null +++ b/tasks/coverage/misc/pass/destructuring-eval-arguments-reads.js @@ -0,0 +1,11 @@ +"use strict"; +let value; +[value = eval] = []; +[value = () => eval] = []; +({ value = eval } = {}); +({ [eval]: value } = {}); +function readArguments() { + [value = arguments] = []; + ({ value = arguments } = {}); + ({ [arguments]: value } = {}); +} diff --git a/tasks/coverage/snapshots/codegen_misc.snap b/tasks/coverage/snapshots/codegen_misc.snap index 6d83a9a37c..63055b737d 100644 --- a/tasks/coverage/snapshots/codegen_misc.snap +++ b/tasks/coverage/snapshots/codegen_misc.snap @@ -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%) diff --git a/tasks/coverage/snapshots/formatter_misc.snap b/tasks/coverage/snapshots/formatter_misc.snap index ee7870e648..c60494f27d 100644 --- a/tasks/coverage/snapshots/formatter_misc.snap +++ b/tasks/coverage/snapshots/formatter_misc.snap @@ -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%) diff --git a/tasks/coverage/snapshots/lexer_misc.snap b/tasks/coverage/snapshots/lexer_misc.snap index 90e5d0b4ab..86e4284302 100644 --- a/tasks/coverage/snapshots/lexer_misc.snap +++ b/tasks/coverage/snapshots/lexer_misc.snap @@ -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%) diff --git a/tasks/coverage/snapshots/parser_misc.snap b/tasks/coverage/snapshots/parser_misc.snap index b872e1fa10..56d89f5b5e 100644 --- a/tasks/coverage/snapshots/parser_misc.snap +++ b/tasks/coverage/snapshots/parser_misc.snap @@ -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) { diff --git a/tasks/coverage/snapshots/semantic_babel.snap b/tasks/coverage/snapshots/semantic_babel.snap index 82fb1a06a5..38cd1cca18 100644 --- a/tasks/coverage/snapshots/semantic_babel.snap +++ b/tasks/coverage/snapshots/semantic_babel.snap @@ -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"] diff --git a/tasks/coverage/snapshots/semantic_misc.snap b/tasks/coverage/snapshots/semantic_misc.snap index c89c4aa898..77d272308c 100644 --- a/tasks/coverage/snapshots/semantic_misc.snap +++ b/tasks/coverage/snapshots/semantic_misc.snap @@ -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"] diff --git a/tasks/coverage/snapshots/transformer_misc.snap b/tasks/coverage/snapshots/transformer_misc.snap index 89853b7176..0281124fbf 100644 --- a/tasks/coverage/snapshots/transformer_misc.snap +++ b/tasks/coverage/snapshots/transformer_misc.snap @@ -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%)