From 9712173521a18fd409a0b6f348aab8a46d520483 Mon Sep 17 00:00:00 2001 From: Amol Bhave Date: Sat, 20 Jun 2026 23:54:55 -0500 Subject: [PATCH] fix: surface error instead of panicking on unclosed exported namespace/module swc's parser recovers from a missing close brace for an unclosed `export namespace`/`export module` without emitting any diagnostic, so it isn't caught by `ensure_no_specific_syntax_errors` and reaches code generation, where `gen_membered_body` panicked via `.expect("Expected to find a close brace token.")`. Push a generation diagnostic (which makes `generate` return an error) instead of panicking when the close brace token is missing. Closes #802 Co-Authored-By: Claude Opus 4.8 --- src/format_text.rs | 55 ++++++++++++++++++++++++++++++++++++++ src/generation/generate.rs | 17 ++++++++---- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/src/format_text.rs b/src/format_text.rs index f13ec55f..11d389a2 100644 --- a/src/format_text.rs +++ b/src/format_text.rs @@ -170,4 +170,59 @@ mod test { "Error formatting tagged template literal at line 1: Syntax error from external formatter" ); } + + fn format_for_test(text: &str) -> Result> { + let config = crate::configuration::ConfigurationBuilder::new().build(); + format_text(FormatTextOptions { + path: &std::path::PathBuf::from("test.ts"), + extension: None, + text: text.into(), + config: &config, + external_formatter: None, + }) + } + + #[test] + fn unclosed_exported_namespace_surfaces_error() { + // unclosed `export namespace`/`export module` is recovered by swc without a diagnostic, + // so it reaches code generation. https://github.com/dprint/dprint-plugin-typescript/issues/802 + for text in [ + "export namespace Foo {\n const x = 1;\n", + "export module Foo {\n const x = 1;\n", + // unclosed empty body + "export namespace Foo {\n", + // nested: the single close brace belongs to the inner block, so the outer is unclosed + "export namespace Foo {\n export namespace Bar {\n const x = 1;\n}\n", + // unclosed outer containing a complete inner construct + "export namespace Foo {\n function bar() {}\n", + // dotted name desugars to a nested namespace declaration + "export namespace Foo.Bar {\n const x = 1;\n", + ] { + let err = format_for_test(text).err().unwrap_or_else(|| panic!("expected an error for {text:?}")); + let message = err.to_string(); + let expected_prefix = "Unexpected end of file. Expected a close brace token for the block starting on line "; + assert!(message.starts_with(expected_prefix), "unexpected message for {text:?}: {message}"); + } + + // pin the full message, including the (1-based) line of the unclosed block's open brace + assert_eq!( + format_for_test("export namespace Foo {\n const x = 1;\n").unwrap_err().to_string(), + "Unexpected end of file. Expected a close brace token for the block starting on line 1." + ); + assert_eq!( + format_for_test("\nexport namespace Foo {\n const x = 1;\n").unwrap_err().to_string(), + "Unexpected end of file. Expected a close brace token for the block starting on line 2." + ); + } + + #[test] + fn closed_namespaces_still_format() { + // regression: well-formed module/namespace declarations must still format. + assert_eq!(format_for_test("export namespace Foo {}\n").unwrap(), None); + assert_eq!(format_for_test("export namespace Foo {\n const x = 1;\n}\n").unwrap(), None); + assert_eq!( + format_for_test("export namespace Foo {\n export namespace Bar {\n const x = 1;\n }\n}\n").unwrap(), + None + ); + } } diff --git a/src/generation/generate.rs b/src/generation/generate.rs index 7fb3a332..fc16a954 100644 --- a/src/generation/generate.rs +++ b/src/generation/generate.rs @@ -7233,11 +7233,18 @@ where .iter() .find(|t| t.token == Token::LBrace) .expect("Expected to find an open brace token."); - let close_brace_token = child_tokens - .iter() - .rev() - .find(|t| t.token == Token::RBrace) - .expect("Expected to find a close brace token."); + let close_brace_token = match child_tokens.iter().rev().find(|t| t.token == Token::RBrace) { + Some(close_brace_token) => close_brace_token, + None => { + context.diagnostics.push(context::GenerateDiagnostic { + message: format!( + "Unexpected end of file. Expected a close brace token for the block starting on line {}.", + open_brace_token.start_line_fast(context.program) + 1 + ), + }); + return items; + } + }; items.extend(gen_brace_separator( GenBraceSeparatorOptions {