diff --git a/crates/oxc_semantic/src/checker/mod.rs b/crates/oxc_semantic/src/checker/mod.rs index 60f350c8e788f..ab5baf1d63372 100644 --- a/crates/oxc_semantic/src/checker/mod.rs +++ b/crates/oxc_semantic/src/checker/mod.rs @@ -105,6 +105,7 @@ pub fn check<'a>(node: &AstNode<'a>, ctx: &SemanticBuilder<'a>) { AstKind::VariableDeclarator(decl) => ts::check_variable_declarator(decl, ctx), AstKind::SimpleAssignmentTarget(target) => ts::check_simple_assignment_target(target, ctx), AstKind::TSInterfaceDeclaration(decl) => ts::check_ts_interface_declaration(decl, ctx), + AstKind::TSTypeAnnotation(annot) => ts::check_ts_type_annotation(annot, ctx), AstKind::TSTypeParameterDeclaration(declaration) => { ts::check_ts_type_parameter_declaration(declaration, ctx); } diff --git a/crates/oxc_semantic/src/checker/typescript.rs b/crates/oxc_semantic/src/checker/typescript.rs index 9e26b01d20995..86940d94eac65 100644 --- a/crates/oxc_semantic/src/checker/typescript.rs +++ b/crates/oxc_semantic/src/checker/typescript.rs @@ -29,6 +29,47 @@ pub fn check_ts_type_parameter_declaration( ctx.error(empty_type_parameter_list(declaration.span)); } } +/// '?' at the end of a type is not valid TypeScript syntax. Did you mean to write 'number | null | undefined'?(17019) +#[allow(clippy::needless_pass_by_value)] +fn jsdoc_type_in_annotation( + modifier: char, + is_start: bool, + span: Span, + suggested_type: Cow, +) -> OxcDiagnostic { + let (code, start_or_end) = if is_start { ("17020", "start") } else { ("17019", "end") }; + + ts_error( + code, + format!("'{modifier}' at the {start_or_end} of a type is not valid TypeScript syntax.",), + ) + .with_label(span) + .with_help(format!("Did you mean to write '{suggested_type}'?")) +} + +pub fn check_ts_type_annotation(annotation: &TSTypeAnnotation<'_>, ctx: &SemanticBuilder<'_>) { + let (modifier, is_start, span_with_illegal_modifier) = match &annotation.type_annotation { + TSType::JSDocNonNullableType(ty) => ('!', !ty.postfix, ty.span()), + TSType::JSDocNullableType(ty) => ('?', !ty.postfix, ty.span()), + _ => { + return; + } + }; + + let valid_type_span = if is_start { + span_with_illegal_modifier.shrink_left(1) + } else { + span_with_illegal_modifier.shrink_right(1) + }; + + let suggestion = if modifier == '?' { + Cow::Owned(format!("{} | null | undefined", &ctx.source_text[valid_type_span])) + } else { + Cow::Borrowed(&ctx.source_text[valid_type_span]) + }; + + ctx.error(jsdoc_type_in_annotation(modifier, is_start, span_with_illegal_modifier, suggestion)); +} /// Initializers are not allowed in ambient contexts. ts(1039) fn initializer_in_ambient_context(init_span: Span) -> OxcDiagnostic { diff --git a/tasks/coverage/snapshots/parser_typescript.snap b/tasks/coverage/snapshots/parser_typescript.snap index 4212ec2e49b55..06b1c60713dcf 100644 --- a/tasks/coverage/snapshots/parser_typescript.snap +++ b/tasks/coverage/snapshots/parser_typescript.snap @@ -3,7 +3,7 @@ commit: a709f989 parser_typescript Summary: AST Parsed : 6469/6479 (99.85%) Positive Passed: 6458/6479 (99.68%) -Negative Passed: 1226/5715 (21.45%) +Negative Passed: 1228/5715 (21.49%) Expect Syntax Error: tasks/coverage/typescript/tests/cases/compiler/ClassDeclaration10.ts Expect Syntax Error: tasks/coverage/typescript/tests/cases/compiler/ClassDeclaration11.ts Expect Syntax Error: tasks/coverage/typescript/tests/cases/compiler/ClassDeclaration13.ts @@ -1663,8 +1663,6 @@ Expect Syntax Error: tasks/coverage/typescript/tests/cases/compiler/parseAssertE Expect Syntax Error: tasks/coverage/typescript/tests/cases/compiler/parseCommaSeparatedNewlineNumber.ts Expect Syntax Error: tasks/coverage/typescript/tests/cases/compiler/parseCommaSeparatedNewlineString.ts Expect Syntax Error: tasks/coverage/typescript/tests/cases/compiler/parseImportAttributesError.ts -Expect Syntax Error: tasks/coverage/typescript/tests/cases/compiler/parseInvalidNonNullableTypes.ts -Expect Syntax Error: tasks/coverage/typescript/tests/cases/compiler/parseInvalidNullableTypes.ts Expect Syntax Error: tasks/coverage/typescript/tests/cases/compiler/parseJsxExtends2.ts Expect Syntax Error: tasks/coverage/typescript/tests/cases/compiler/parseTypes.ts Expect Syntax Error: tasks/coverage/typescript/tests/cases/compiler/partialDiscriminatedUnionMemberHasGoodError.ts @@ -10185,6 +10183,173 @@ Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/salsa/private ╰──── help: Try insert a semicolon here + × TS(17019): '!' at the end of a type is not valid TypeScript syntax. + ╭─[typescript/tests/cases/compiler/parseInvalidNonNullableTypes.ts:1:30] + 1 │ function f1(a: string): a is string! { + · ─────── + 2 │ return true; + ╰──── + help: Did you mean to write 'string'? + + × TS(17020): '!' at the start of a type is not valid TypeScript syntax. + ╭─[typescript/tests/cases/compiler/parseInvalidNonNullableTypes.ts:5:30] + 4 │ + 5 │ function f2(a: string): a is !string { + · ─────── + 6 │ return true; + ╰──── + help: Did you mean to write 'string'? + + × TS(17019): '!' at the end of a type is not valid TypeScript syntax. + ╭─[typescript/tests/cases/compiler/parseInvalidNonNullableTypes.ts:9:16] + 8 │ + 9 │ function f3(a: string!) {} + · ─────── + 10 │ function f4(a: number!) {} + ╰──── + help: Did you mean to write 'string'? + + × TS(17019): '!' at the end of a type is not valid TypeScript syntax. + ╭─[typescript/tests/cases/compiler/parseInvalidNonNullableTypes.ts:10:16] + 9 │ function f3(a: string!) {} + 10 │ function f4(a: number!) {} + · ─────── + 11 │ + ╰──── + help: Did you mean to write 'number'? + + × TS(17020): '!' at the start of a type is not valid TypeScript syntax. + ╭─[typescript/tests/cases/compiler/parseInvalidNonNullableTypes.ts:12:16] + 11 │ + 12 │ function f5(a: !string) {} + · ─────── + 13 │ function f6(a: !number) {} + ╰──── + help: Did you mean to write 'string'? + + × TS(17020): '!' at the start of a type is not valid TypeScript syntax. + ╭─[typescript/tests/cases/compiler/parseInvalidNonNullableTypes.ts:13:16] + 12 │ function f5(a: !string) {} + 13 │ function f6(a: !number) {} + · ─────── + 14 │ + ╰──── + help: Did you mean to write 'number'? + + × TS(17019): '!' at the end of a type is not valid TypeScript syntax. + ╭─[typescript/tests/cases/compiler/parseInvalidNonNullableTypes.ts:15:16] + 14 │ + 15 │ function f7(): string! {} + · ─────── + 16 │ function f8(): !string {} + ╰──── + help: Did you mean to write 'string'? + + × TS(17020): '!' at the start of a type is not valid TypeScript syntax. + ╭─[typescript/tests/cases/compiler/parseInvalidNonNullableTypes.ts:16:16] + 15 │ function f7(): string! {} + 16 │ function f8(): !string {} + · ─────── + 17 │ + ╰──── + help: Did you mean to write 'string'? + + × TS(17019): '!' at the end of a type is not valid TypeScript syntax. + ╭─[typescript/tests/cases/compiler/parseInvalidNonNullableTypes.ts:19:10] + 18 │ const a = 1 as any!; + 19 │ const b: number! = 1; + · ─────── + 20 │ + ╰──── + help: Did you mean to write 'number'? + + × TS(17020): '!' at the start of a type is not valid TypeScript syntax. + ╭─[typescript/tests/cases/compiler/parseInvalidNonNullableTypes.ts:22:10] + 21 │ const c = 1 as !any; + 22 │ const d: !number = 1; + · ─────── + ╰──── + help: Did you mean to write 'number'? + + × TS(17020): '?' at the start of a type is not valid TypeScript syntax. + ╭─[typescript/tests/cases/compiler/parseInvalidNullableTypes.ts:1:30] + 1 │ function f1(a: string): a is ?string { + · ─────── + 2 │ return true; + ╰──── + help: Did you mean to write 'string | null | undefined'? + + × TS(17019): '?' at the end of a type is not valid TypeScript syntax. + ╭─[typescript/tests/cases/compiler/parseInvalidNullableTypes.ts:5:16] + 4 │ + 5 │ function f2(a: string?) {} + · ─────── + 6 │ function f3(a: number?) {} + ╰──── + help: Did you mean to write 'string | null | undefined'? + + × TS(17019): '?' at the end of a type is not valid TypeScript syntax. + ╭─[typescript/tests/cases/compiler/parseInvalidNullableTypes.ts:6:16] + 5 │ function f2(a: string?) {} + 6 │ function f3(a: number?) {} + · ─────── + 7 │ + ╰──── + help: Did you mean to write 'number | null | undefined'? + + × TS(17020): '?' at the start of a type is not valid TypeScript syntax. + ╭─[typescript/tests/cases/compiler/parseInvalidNullableTypes.ts:11:25] + 10 │ + 11 │ function f6(a: string): ?string { + · ─────── + 12 │ return true; + ╰──── + help: Did you mean to write 'string | null | undefined'? + + × TS(17019): '?' at the end of a type is not valid TypeScript syntax. + ╭─[typescript/tests/cases/compiler/parseInvalidNullableTypes.ts:16:10] + 15 │ const a = 1 as any?; + 16 │ const b: number? = 1; + · ─────── + 17 │ + ╰──── + help: Did you mean to write 'number | null | undefined'? + + × TS(17019): '?' at the end of a type is not valid TypeScript syntax. + ╭─[typescript/tests/cases/compiler/parseInvalidNullableTypes.ts:21:8] + 20 │ + 21 │ let e: unknown?; + · ──────── + 22 │ let f: never?; + ╰──── + help: Did you mean to write 'unknown | null | undefined'? + + × TS(17019): '?' at the end of a type is not valid TypeScript syntax. + ╭─[typescript/tests/cases/compiler/parseInvalidNullableTypes.ts:22:8] + 21 │ let e: unknown?; + 22 │ let f: never?; + · ────── + 23 │ let g: void?; + ╰──── + help: Did you mean to write 'never | null | undefined'? + + × TS(17019): '?' at the end of a type is not valid TypeScript syntax. + ╭─[typescript/tests/cases/compiler/parseInvalidNullableTypes.ts:23:8] + 22 │ let f: never?; + 23 │ let g: void?; + · ───── + 24 │ let h: undefined?; + ╰──── + help: Did you mean to write 'void | null | undefined'? + + × TS(17019): '?' at the end of a type is not valid TypeScript syntax. + ╭─[typescript/tests/cases/compiler/parseInvalidNullableTypes.ts:24:8] + 23 │ let g: void?; + 24 │ let h: undefined?; + · ────────── + ╰──── + help: Did you mean to write 'undefined | null | undefined'? + × Unexpected token ╭─[typescript/tests/cases/compiler/parseJsxElementInUnaryExpressionNoCrash1.ts:1:4] 1 │ ~< <