From c7662e94fce0b163c56a574d920e503aa9fa5f8d Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 30 Oct 2026 18:13:06 -0500 Subject: [PATCH 1/3] fix(parser): JSDoc @template replaces an empty type parameter list Go reparser.go:459 tests `fun.TypeParameters() == nil`, the Nodes of the list, which the Go parser leaves nil for an empty `<>` (Arena.Clone of an empty slice is nil). The port tested the list itself, so `/** @template T */ function f<>() {}` in a JS file kept the empty list: TS1098, TS2304 for the template names, and no type parameters in the declaration emit. The class branches test the list in Go too and are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../ts_goport/src/frontend/parser/reparser.rs | 5 +- .../tsctests/empty_type_parameters.rs | 112 ++++++++++++++++++ .../tests/go_baselines/tsctests/mod.rs | 1 + 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 crates/ts_goport/tests/go_baselines/tsctests/empty_type_parameters.rs diff --git a/crates/ts_goport/src/frontend/parser/reparser.rs b/crates/ts_goport/src/frontend/parser/reparser.rs index 74aab83d5..381c91c29 100644 --- a/crates/ts_goport/src/frontend/parser/reparser.rs +++ b/crates/ts_goport/src/frontend/parser/reparser.rs @@ -841,7 +841,10 @@ impl<'a> Parser<'a> { SyntaxKind::JsDocTemplateTag => { let fun = get_function_like_host(parent); if fun.is_some() { - if fun.type_parameter_list().is_nil() && fun.full_signature().is_nil() { + // PORT: Go `fun.TypeParameters() == nil` tests the list's Nodes, which + // the Go parser leaves nil for an empty `<>` (Arena.Clone of an empty + // slice is nil), so an empty list is replaced too. + if fun.type_parameters().is_empty() && fun.full_signature().is_nil() { let type_parameters = self.gather_type_parameters(js_doc, false /*typedefOrCallback*/); set_function_type_parameters(fun, type_parameters); diff --git a/crates/ts_goport/tests/go_baselines/tsctests/empty_type_parameters.rs b/crates/ts_goport/tests/go_baselines/tsctests/empty_type_parameters.rs new file mode 100644 index 000000000..94e8a5aa6 --- /dev/null +++ b/crates/ts_goport/tests/go_baselines/tsctests/empty_type_parameters.rs @@ -0,0 +1,112 @@ +//! Port-only test of an empty type parameter list `<>`. +//! +//! PORT: no Go counterpart. Go `(*Node).TypeParameters()` (ast/ast.go:522) +//! gives the Nodes of the list, and the Go parser stores nil Nodes for an +//! empty list (`Arena.Clone` of an empty slice is nil, core/arena.go:55). So +//! `TypeParameters() == nil` holds for `f<>()`. The reparser +//! (parser/reparser.go:459) read the list instead: a JSDoc `@template` did +//! not replace the empty list of a function, so the file got TS1098 and the +//! template names were not found. +//! +//! Every expected output is the output of `tsgo-oracle-3e9236361fa9 -p +//! tsconfig.json --pretty false` on the same files. + +use ts_goport::execute::tsc::ExitStatus; + +use crate::support::child::run_command_in_child; +use crate::support::runner::TscInput; +use crate::support::test_sys::{TestSys, new_test_sys}; + +const PROJECT: &str = "/home/src/workspaces/project"; + +const CHECK_JS: &str = + r#"{"compilerOptions":{"noEmit":true,"allowJs":true,"checkJs":true},"files":["a.js"]}"#; + +/// Runs `tsgo -p tsconfig.json --pretty false` on `file` (`a.js` or `a.ts`) +/// with `tsconfig` and gives the system, the status and the diagnostics text. +fn tsc(file: &str, text: &str, tsconfig: &str) -> (TestSys, ExitStatus, String) { + let input = TscInput { + files: [ + (format!("{PROJECT}/{file}"), text.into()), + (format!("{PROJECT}/tsconfig.json"), tsconfig.into()), + ] + .into_iter() + .collect(), + ..Default::default() + }; + let sys = new_test_sys(&input, false); + let args = ["-p", "tsconfig.json", "--pretty", "false"].map(String::from); + let result = run_command_in_child(&sys, &args).unwrap_or_else(|err| panic!("tsgo: {err}")); + assert!(result.unported.is_none(), "unported {:?}", result.unported); + // The test system adds the list of files after the diagnostics. + let output = sys.output_text(); + let diagnostics = output + .split("!!! List files start") + .next() + .unwrap_or_default() + .to_string(); + (sys, result.status, diagnostics) +} + +/// The fuzzer's repro, and `<>` with a space or a comment inside. +#[test] +fn template_tag_replaces_an_empty_type_parameter_list() { + for text in [ + "/** @template T */\nfunction f<>() {}\n", + "/** @template T */\nfunction f< >() {}\n/** @template T */\nfunction g() {}\n", + ] { + let (_, status, diagnostics) = tsc("a.js", text, CHECK_JS); + assert_eq!(diagnostics, "", "{text}"); + assert_eq!(status, ExitStatus::Success, "{text}"); + } +} + +/// The template name is found (no TS2304) in a return tag, a method, a +/// function expression and a parameter tag. +#[test] +fn template_tag_names_are_found_with_an_empty_list() { + let cases = [ + ( + "/**\n * @template T\n * @returns {T}\n */\nfunction f<>() {}\n", + "a.js(3,14): error TS2355: A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value.\n", + ), + ( + r#"class C { + /** @template T */ + m<>() {} +} +const g = /** @template T */ function <>() {}; +/** @template T @param {T} x @returns {T} */ +function h<>(x) { return x; } +/** @type {number} */ +const n = h("s"); +"#, + "a.js(9,7): error TS2322: Type 'string' is not assignable to type 'number'.\n", + ), + ]; + for (text, want) in cases { + let (_, status, diagnostics) = tsc("a.js", text, CHECK_JS); + assert_eq!(diagnostics, want, "{text}"); + assert_eq!( + status, + ExitStatus::DiagnosticsPresentOutputsGenerated, + "{text}" + ); + } +} + +/// The declaration emit has the template type parameters. +#[test] +fn template_tag_type_parameters_are_emitted_for_an_empty_list() { + let (sys, status, diagnostics) = tsc( + "a.js", + "/** @template T */\nfunction f<>() {}\n/** @template T @param {T} x */\nfunction g< >(x) {}\n", + r#"{"compilerOptions":{"allowJs":true,"declaration":true,"emitDeclarationOnly":true,"outDir":"out"},"files":["a.js"]}"#, + ); + assert_eq!(diagnostics, ""); + assert_eq!(status, ExitStatus::Success); + assert_eq!( + sys.read_file_no_error(&format!("{PROJECT}/out/a.d.ts")), + "/** @template T */\ndeclare function f(): void;\n/** @template T @param {T} x */\ndeclare function g(x: T): void;\n" + ); +} diff --git a/crates/ts_goport/tests/go_baselines/tsctests/mod.rs b/crates/ts_goport/tests/go_baselines/tsctests/mod.rs index 2f3d421fb..6a3b44733 100644 --- a/crates/ts_goport/tests/go_baselines/tsctests/mod.rs +++ b/crates/ts_goport/tests/go_baselines/tsctests/mod.rs @@ -4,6 +4,7 @@ mod build_prefetch_rereads; mod checker_common_source_directory; mod contentmapper_watch; mod effect_build_info; +mod empty_type_parameters; mod explain_files_cache; mod extended_counts; mod file_delete; From e571908b2c36ffb5beb6f436aa98d633fabb059e Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 30 Oct 2026 18:13:06 -0500 Subject: [PATCH 2/3] fix(checker): a function with an empty `<>` infers from annotated parameters Go checker.go:10415 tests `node.TypeParameters() == nil`, which is also true for an empty `<>` list. The port tested the list, so inferFromAnnotatedParametersAndReturn did not run for `function <>(x: A)` and the inferred type could differ (`string` where Go gives `string | number`). Co-Authored-By: Claude Opus 5.5 --- crates/ts_goport/src/checker/checker_p11.rs | 5 ++- .../tsctests/empty_type_parameters.rs | 38 +++++++++++++++++-- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/crates/ts_goport/src/checker/checker_p11.rs b/crates/ts_goport/src/checker/checker_p11.rs index 149059709..dcfcd9208 100644 --- a/crates/ts_goport/src/checker/checker_p11.rs +++ b/crates/ts_goport/src/checker/checker_p11.rs @@ -1536,10 +1536,11 @@ impl Checker { self.assign_non_contextual_parameter_types(signature); } } else if contextual_signature.is_some() - && node.type_parameter_list().is_nil() + && node.type_parameters().is_empty() && self.sig(contextual_signature).parameters.len() > node.parameters().len() { - // PORT: Go `node.TypeParameters() == nil` is nil exactly when the list is nil. + // PORT: Go `node.TypeParameters() == nil` is also nil for an empty `<>` + // list (the Go parser stores nil Nodes for an empty list). let inference_context = self.get_inference_context(node); if check_mode.intersects(CheckMode::INFERENTIAL) { self.infer_from_annotated_parameters_and_return( diff --git a/crates/ts_goport/tests/go_baselines/tsctests/empty_type_parameters.rs b/crates/ts_goport/tests/go_baselines/tsctests/empty_type_parameters.rs index 94e8a5aa6..644bfb76e 100644 --- a/crates/ts_goport/tests/go_baselines/tsctests/empty_type_parameters.rs +++ b/crates/ts_goport/tests/go_baselines/tsctests/empty_type_parameters.rs @@ -3,10 +3,13 @@ //! PORT: no Go counterpart. Go `(*Node).TypeParameters()` (ast/ast.go:522) //! gives the Nodes of the list, and the Go parser stores nil Nodes for an //! empty list (`Arena.Clone` of an empty slice is nil, core/arena.go:55). So -//! `TypeParameters() == nil` holds for `f<>()`. The reparser -//! (parser/reparser.go:459) read the list instead: a JSDoc `@template` did -//! not replace the empty list of a function, so the file got TS1098 and the -//! template names were not found. +//! `TypeParameters() == nil` holds for `f<>()`. Two ported tests read the +//! list instead and saw `<>` as type parameters: +//! - the reparser (parser/reparser.go:459): a JSDoc `@template` did not +//! replace the empty list of a function, so the file got TS1098 and the +//! template names were not found. +//! - checkFunctionExpressionOrObjectLiteralMethod (checker/checker.go:10415): +//! a function with `<>` gave no inferences from its annotated parameters. //! //! Every expected output is the output of `tsgo-oracle-3e9236361fa9 -p //! tsconfig.json --pretty false` on the same files. @@ -110,3 +113,30 @@ fn template_tag_type_parameters_are_emitted_for_an_empty_list() { "/** @template T */\ndeclare function f(): void;\n/** @template T @param {T} x */\ndeclare function g(x: T): void;\n" ); } + +/// A function expression with `<>` infers from its annotated parameters (a +/// covariant candidate, so `T` is `string | number`, not `string`), as one +/// without type parameters does. +#[test] +fn empty_type_parameter_list_infers_from_annotated_parameters() { + let (_, status, diagnostics) = tsc( + "a.ts", + r#"declare function g(cb: (x: T, y: number) => void, v: T): T; +const r = g(function <>(x: string | number) {}, "a"); +const z: boolean = r; +const r2 = g((x: string | number) => {}, "a"); +const z2: boolean = r2; +"#, + r#"{"compilerOptions":{"noEmit":true,"strict":true},"files":["a.ts"]}"#, + ); + assert_eq!( + diagnostics, + r#"a.ts(2,22): error TS1098: Type parameter list cannot be empty. +a.ts(3,7): error TS2322: Type 'string | number' is not assignable to type 'boolean'. + Type 'string' is not assignable to type 'boolean'. +a.ts(5,7): error TS2322: Type 'string | number' is not assignable to type 'boolean'. + Type 'string' is not assignable to type 'boolean'. +"# + ); + assert_eq!(status, ExitStatus::DiagnosticsPresentOutputsGenerated); +} From d4861346cecd02e3810612705cb3978dc4f5528b Mon Sep 17 00:00:00 2001 From: Chad Elliott Date: Fri, 30 Oct 2026 18:59:55 -0500 Subject: [PATCH 3/3] chore(goport): lib blobs for the empty type parameter list fix frontend/parser/reparser.rs is a key source of lib_parse.bin. Co-Authored-By: Claude Opus 5.5 --- crates/ts_goport/src/binder/lib_bind.bin | Bin 1188618 -> 1188618 bytes .../src/frontend/parser/lib_parse.bin | Bin 1435109 -> 1435109 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/crates/ts_goport/src/binder/lib_bind.bin b/crates/ts_goport/src/binder/lib_bind.bin index b2a87217854273c790e2bbfa6d072f934ea486bb..4e5fe7241ce717ce696d17c348e70ce902b9feed 100644 GIT binary patch delta 88 zcmeA==FxS`V}b%leP|pPyLX&VqeiO+W2**Js|Itc21~03YpVuZs|I_k21ly~XR8KR ls|I(g22ZO7Z>t7hs|Np84T1UoaLw)K{ROt4_ZPIj3;=%o8Yut( delta 88 zcmeA==FxS`V}b(5@(NE@{T*>T8Z}xq7+W=%S~ZwkHCS3TSX(vNS~b{PH8@%|I9oNi lS~a*^HF#Pzcw062S~d8$Y6#5thih&>?=P_ZyuYCJWdP@}9E|_~ diff --git a/crates/ts_goport/src/frontend/parser/lib_parse.bin b/crates/ts_goport/src/frontend/parser/lib_parse.bin index 57cdd6232eee946e6b0796b19d869ce9808a80f8..eba29c80204c2ae17fdb4ef322890c5cbf6fab28 100644 GIT binary patch delta 100 zcmaEQI{fMB@CgbWZj-B7|C{dL(x}m@!Pu(7)T+VUs=?B#!P=_9)~dnYs=?8!!P%<8 y)vCeWs=?E$!P~0A*Q&wasv*#-A=s)R)T$x8RYN3$7j97d3|^7#Gk8UPZvX(Ioghg7 delta 100 zcmaEQI{fMB@CgbWI+cbUU$$=WZq#ViU~JW3YSmzF)nIAWU~Sc4Yt>+H)!=B=;B3|4 xYSrLw)!=E>;BD35Yt`Uy)evaa5Ny>DYSj?lsv(lW3pc2J2CvBW8N8ytHvprHAWZ-O