diff --git a/crates/ts_goport/src/binder/lib_bind.bin b/crates/ts_goport/src/binder/lib_bind.bin index b2a872178..4e5fe7241 100644 Binary files a/crates/ts_goport/src/binder/lib_bind.bin and b/crates/ts_goport/src/binder/lib_bind.bin differ 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/src/frontend/parser/lib_parse.bin b/crates/ts_goport/src/frontend/parser/lib_parse.bin index 57cdd6232..eba29c802 100644 Binary files a/crates/ts_goport/src/frontend/parser/lib_parse.bin and b/crates/ts_goport/src/frontend/parser/lib_parse.bin differ 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..644bfb76e --- /dev/null +++ b/crates/ts_goport/tests/go_baselines/tsctests/empty_type_parameters.rs @@ -0,0 +1,142 @@ +//! 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<>()`. 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. + +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" + ); +} + +/// 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); +} 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;