From 0eff6be0a0a05e31b01fbfc110feebe418b0d642 Mon Sep 17 00:00:00 2001 From: Boshen Date: Wed, 4 Feb 2026 05:21:42 +0000 Subject: [PATCH] feat(parser): error JSX-like type assertions and generics in `.mts`/`.cts` (#18910) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Implements TS7059 and TS7060 errors for JSX-like syntax in `.mts`/`.cts` files: - **TS7059**: JSX-like type assertions (`value`) - "Use an `as` expression instead" - **TS7060**: JSX-like type parameters in arrow functions (`() => {}`) - "Add a trailing comma or explicit constraint" ### Valid workarounds for TS7060: - `() => {}` (trailing comma) - `() => {}` (explicit constraint) - `() => {}` (multiple type parameters) Closes #15184 ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) --- crates/oxc_parser/src/diagnostics.rs | 20 +++++++++++++ crates/oxc_parser/src/js/arrow.rs | 14 ++++++++-- crates/oxc_parser/src/ts/statement.rs | 10 +++++-- crates/oxc_parser/src/ts/types.rs | 22 ++++++++++++--- .../coverage/snapshots/parser_typescript.snap | 28 +++++++++++++++++-- .../snapshots/babel.snap.md | 4 +-- 6 files changed, 85 insertions(+), 13 deletions(-) diff --git a/crates/oxc_parser/src/diagnostics.rs b/crates/oxc_parser/src/diagnostics.rs index 5abf5e2577717..c4bf822e2126e 100644 --- a/crates/oxc_parser/src/diagnostics.rs +++ b/crates/oxc_parser/src/diagnostics.rs @@ -1284,3 +1284,23 @@ pub fn ts_import_type_options_invalid_key(span: Span) -> OxcDiagnostic { pub fn ts_import_type_options_no_spread(span: Span) -> OxcDiagnostic { OxcDiagnostic::error("Spread elements are not allowed in import type options.").with_label(span) } + +// This syntax is reserved in files with the .mts or .cts extension. Use an `as` expression instead. ts(7059) +#[cold] +pub fn jsx_type_assertion_in_mts_cts(span: Span) -> OxcDiagnostic { + ts_error( + "7059", + "This syntax is reserved in files with the .mts or .cts extension. Use an `as` expression instead.", + ) + .with_label(span) +} + +// This syntax is reserved in files with the .mts or .cts extension. Add a trailing comma or explicit constraint. ts(7060) +#[cold] +pub fn jsx_type_parameter_in_mts_cts(span: Span) -> OxcDiagnostic { + ts_error( + "7060", + "This syntax is reserved in files with the .mts or .cts extension. Add a trailing comma or explicit constraint.", + ) + .with_label(span) +} diff --git a/crates/oxc_parser/src/js/arrow.rs b/crates/oxc_parser/src/js/arrow.rs index 788371f60cd72..0ef09bf6993fa 100644 --- a/crates/oxc_parser/src/js/arrow.rs +++ b/crates/oxc_parser/src/js/arrow.rs @@ -1,6 +1,6 @@ use oxc_allocator::Box; use oxc_ast::{NONE, ast::*}; -use oxc_span::GetSpan; +use oxc_span::{FileExtension, GetSpan}; use oxc_syntax::precedence::Precedence; use super::{FunctionKind, Tristate}; @@ -252,7 +252,17 @@ impl<'a> ParserImpl<'a> { let has_await = self.ctx.has_await(); self.ctx = self.ctx.union_await_if(r#async); - let type_parameters = self.parse_ts_type_parameters(); + let (type_parameters, has_trailing_comma) = + self.parse_ts_type_parameters_with_trailing_comma(); + + if let Some(type_params) = &type_parameters + && matches!(self.source_type.extension(), Some(FileExtension::Mts | FileExtension::Cts)) + && type_params.params.len() == 1 + && type_params.params[0].constraint.is_none() + && !has_trailing_comma + { + self.error(diagnostics::jsx_type_parameter_in_mts_cts(type_params.params[0].name.span)); + } let (this_param, params) = self.parse_formal_parameters( FunctionKind::Expression, diff --git a/crates/oxc_parser/src/ts/statement.rs b/crates/oxc_parser/src/ts/statement.rs index da7f0d4a18110..b9c9e549d981f 100644 --- a/crates/oxc_parser/src/ts/statement.rs +++ b/crates/oxc_parser/src/ts/statement.rs @@ -1,6 +1,6 @@ use oxc_allocator::{Box, Vec}; use oxc_ast::ast::*; -use oxc_span::GetSpan; +use oxc_span::{FileExtension, GetSpan}; use crate::{ Context, ParserImpl, diagnostics, @@ -552,7 +552,13 @@ impl<'a> ParserImpl<'a> { self.expect(Kind::RAngle); let lhs_span = self.start_span(); let expression = self.parse_simple_unary_expression(lhs_span); - self.ast.expression_ts_type_assertion(self.end_span(span), type_annotation, expression) + let span = self.end_span(span); + + if matches!(self.source_type.extension(), Some(FileExtension::Mts | FileExtension::Cts)) { + self.error(diagnostics::jsx_type_assertion_in_mts_cts(span)); + } + + self.ast.expression_ts_type_assertion(span, type_annotation, expression) } pub(crate) fn parse_ts_import_equals_declaration( diff --git a/crates/oxc_parser/src/ts/types.rs b/crates/oxc_parser/src/ts/types.rs index e9a5652a4c28d..d7ee5e40148b5 100644 --- a/crates/oxc_parser/src/ts/types.rs +++ b/crates/oxc_parser/src/ts/types.rs @@ -151,16 +151,30 @@ impl<'a> ParserImpl<'a> { pub(crate) fn parse_ts_type_parameters( &mut self, ) -> Option>> { + self.parse_ts_type_parameters_impl().0 + } + + /// Parse TypeScript type parameters and return whether there was a trailing comma. + /// Used for arrow functions to check for TS7060 (JSX-like type parameters in .mts/.cts). + pub(crate) fn parse_ts_type_parameters_with_trailing_comma( + &mut self, + ) -> (Option>>, bool) { + self.parse_ts_type_parameters_impl() + } + + fn parse_ts_type_parameters_impl( + &mut self, + ) -> (Option>>, bool) { if !self.is_ts { - return None; + return (None, false); } if !self.at(Kind::LAngle) { - return None; + return (None, false); } let span = self.start_span(); let opening_span = self.cur_token().span(); self.expect(Kind::LAngle); - let (params, _) = self.parse_delimited_list( + let (params, trailing_comma) = self.parse_delimited_list( Kind::RAngle, Kind::Comma, opening_span, @@ -171,7 +185,7 @@ impl<'a> ParserImpl<'a> { if params.is_empty() { self.error(diagnostics::ts_empty_type_parameter_list(span)); } - Some(self.ast.alloc_ts_type_parameter_declaration(span, params)) + (Some(self.ast.alloc_ts_type_parameter_declaration(span, params)), trailing_comma.is_some()) } pub(crate) fn parse_ts_implements_clause(&mut self) -> Vec<'a, TSClassImplements<'a>> { diff --git a/tasks/coverage/snapshots/parser_typescript.snap b/tasks/coverage/snapshots/parser_typescript.snap index b2fab81612855..2e27d96c9d270 100644 --- a/tasks/coverage/snapshots/parser_typescript.snap +++ b/tasks/coverage/snapshots/parser_typescript.snap @@ -3,7 +3,7 @@ commit: 95e3aaa9 parser_typescript Summary: AST Parsed : 9842/9843 (99.99%) Positive Passed: 9838/9843 (99.95%) -Negative Passed: 1514/2557 (59.21%) +Negative Passed: 1515/2557 (59.25%) Expect Syntax Error: tasks/coverage/typescript/tests/cases/compiler/FunctionDeclaration3.ts Expect Syntax Error: tasks/coverage/typescript/tests/cases/compiler/FunctionDeclaration4.ts @@ -1690,8 +1690,6 @@ Expect Syntax Error: tasks/coverage/typescript/tests/cases/conformance/node/node Expect Syntax Error: tasks/coverage/typescript/tests/cases/conformance/node/nodeModulesExportsBlocksTypesVersions.ts -Expect Syntax Error: tasks/coverage/typescript/tests/cases/conformance/node/nodeModulesForbidenSyntax.ts - Expect Syntax Error: tasks/coverage/typescript/tests/cases/conformance/node/nodeModulesGeneratedNameCollisions.ts Expect Syntax Error: tasks/coverage/typescript/tests/cases/conformance/node/nodeModulesImportAssertions.ts @@ -21115,6 +21113,30 @@ Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/statements/Va โ•ฐโ”€โ”€โ”€โ”€ help: TypeScript transforms 'import ... =' to 'const ... =' + ร— TS(7060): This syntax is reserved in files with the .mts or .cts extension. Add a trailing comma or explicit constraint. + โ•ญโ”€[typescript/tests/cases/conformance/node/nodeModulesForbidenSyntax.ts:2:12] + 1 โ”‚ // cjs format file + 2 โ”‚ const x = () => (void 0); + ยท โ”€ + 3 โ”‚ export {x}; + โ•ฐโ”€โ”€โ”€โ”€ + + ร— TS(7059): This syntax is reserved in files with the .mts or .cts extension. Use an `as` expression instead. + โ•ญโ”€[typescript/tests/cases/conformance/node/nodeModulesForbidenSyntax.ts:2:23] + 1 โ”‚ // cjs format file + 2 โ”‚ const x = () => (void 0); + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + 3 โ”‚ export {x}; + โ•ฐโ”€โ”€โ”€โ”€ + + ร— TS(7059): This syntax is reserved in files with the .mts or .cts extension. Use an `as` expression instead. + โ•ญโ”€[typescript/tests/cases/conformance/node/nodeModulesForbidenSyntax.ts:2:20] + 1 โ”‚ // cjs format file + 2 โ”‚ const x = () => (void 0); + ยท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + 3 โ”‚ export {x}; + โ•ฐโ”€โ”€โ”€โ”€ + ร— Expected 'with' in import type options โ•ญโ”€[typescript/tests/cases/conformance/node/nodeModulesImportAttributesTypeModeDeclarationEmitErrors.ts:3:22] 2 โ”‚ export type LocalInterface = diff --git a/tasks/transform_conformance/snapshots/babel.snap.md b/tasks/transform_conformance/snapshots/babel.snap.md index d5f6aadaaab94..ba3a7990c1da6 100644 --- a/tasks/transform_conformance/snapshots/babel.snap.md +++ b/tasks/transform_conformance/snapshots/babel.snap.md @@ -1,6 +1,6 @@ commit: 92c052dc -Passed: 699/1164 +Passed: 695/1160 # All Passed: * babel-plugin-transform-logical-assignment-operators @@ -1311,7 +1311,7 @@ x Output mismatch x Output mismatch -# babel-preset-typescript (11/16) +# babel-preset-typescript (7/12) * node-extensions/import-in-cts/input.cts x Output mismatch