Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified crates/ts_goport/src/binder/lib_bind.bin
Binary file not shown.
5 changes: 3 additions & 2 deletions crates/ts_goport/src/checker/checker_p11.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Binary file modified crates/ts_goport/src/frontend/parser/lib_parse.bin
Binary file not shown.
5 changes: 4 additions & 1 deletion crates/ts_goport/src/frontend/parser/reparser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
142 changes: 142 additions & 0 deletions crates/ts_goport/tests/go_baselines/tsctests/empty_type_parameters.rs
Original file line number Diff line number Diff line change
@@ -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</* c */>() {}\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<T>(): void;\n/** @template T @param {T} x */\ndeclare function g<T>(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<T>(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);
}
1 change: 1 addition & 0 deletions crates/ts_goport/tests/go_baselines/tsctests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down