Skip to content

Commit a0d46cd

Browse files
authored
fix: strip trailing whitespace on codegenned new lines (#4639)
## Motivation and Context Part of the solution for #4634 Generated Rust server code contains whitespace-only lines that cause `cargo fmt` to fail with `error[internal]: left behind trailing whitespace`. This reproduces on the stock `smithy-rust-quickstart` template with no modifications. ## Description Smithy's `AbstractCodeWriter` prepends the current indentation to every line it writes, including blank lines. When codegen templates produce empty lines (via `join("\n")` separators between writables, or empty string interpolations like `$boxIt`/`$boxErr`), the writer turns them into whitespace-only lines. This change strips trailing whitespace (spaces and tabs before a newline) from all lines in `RustWriter.toString()` using a single regex replace. Blank lines are preserved; only the invisible indentation on them is removed. This is a global fix that catches all current and future instances rather than patching individual templates. The alternative would be to patch each template that produces blank lines (the `join("\n")` separator in `SmithyValidationExceptionDecorator`, the `$boxIt`/`$boxErr` interpolations in `UnconstrainedUnionGenerator`, etc.). That approach is fragile since any new template could reintroduce the problem. The `toString()` approach is a single chokepoint that all generated output passes through. The performance cost is negligible: the regex (`[ \t]+\n`) is pre-compiled as a `companion object` val, and it runs a simple character class scan with no backtracking. `toString()` is called once per output file during `flushWriters()` (plus once per inline module writer). In the `codegen-server-test` suite (~2500 generated `.rs` files, ~18MB total), this adds no measurable overhead compared to the model traversal, template rendering, and `cargo fmt` invocation that dominate the build. This is a partial fix: it eliminates all trailing whitespace produced by codegen, which resolves the whitespace-only blank lines described in the issue. However, both reproducers in the issue also generate `pub(crate)` tuple structs with long type paths (e.g. constrained map/collection wrappers), and `rustfmt` itself reintroduces trailing whitespace when wrapping those fields. That means `cargo fmt` will still fail on a freshly generated workspace for models that produce these types. That is an upstream `rustfmt` bug ([rust-lang/rustfmt#6880](rust-lang/rustfmt#6880)). The codegen pipeline already catches and logs `cargo fmt` failures, so those cases are non-blocking. ## Testing Added a regression test in `RustWriterTest` that reproduces both patterns from the issue (join separator blank lines and empty string interpolation blank lines inside indented blocks) and asserts no trailing whitespace in the output. Full `codegen-core` test suite passes. Verified end-to-end with a clean `codegen-server-test:assemble` build (~2500 generated `.rs` files). All codegen-produced trailing whitespace is eliminated. The only remaining trailing whitespace in generated files comes from `rustfmt` reintroducing it on `pub(crate)` tuple struct fields (the upstream bug). Benchmarked `codegen-server-test:clean assemble` over 10 runs each: 25.2s +/- 0.6s with the fix vs 24.9s +/- 0.7s without. Within noise, no measurable performance impact. ## Checklist - [x] For changes to the smithy-rs codegen or runtime crates, I have created a changelog entry Markdown file in the `.changelog` directory, specifying "client," "server," or both in the `applies_to` key. ---- _By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice._
1 parent 777c24b commit a0d46cd

3 files changed

Lines changed: 49 additions & 0 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
applies_to:
3+
- server
4+
authors:
5+
- jlizen
6+
references:
7+
- smithy-rs#4634
8+
breaking: false
9+
new_feature: false
10+
bug_fix: true
11+
---
12+
13+
Strip trailing whitespace from generated Rust code. Smithy's `AbstractCodeWriter` adds indentation to blank lines, producing whitespace-only lines that cause `cargo fmt` to fail with `error[internal]: left behind trailing whitespace`.

codegen-core/src/main/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriter.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,8 @@ class RustWriter private constructor(
598598
) :
599599
SymbolWriter<RustWriter, UseDeclarations>(UseDeclarations(namespace)) {
600600
companion object {
601+
private val TRAILING_WHITESPACE_RE = Regex("[ \\t]+\\n")
602+
601603
fun root() = forModule(null)
602604

603605
fun forModule(module: String?): RustWriter =
@@ -925,6 +927,7 @@ class RustWriter private constructor(
925927
null
926928
}
927929
return listOfNotNull(preheader, header, useDecls, contents).joinToString(separator = "\n", postfix = "\n")
930+
.replace(TRAILING_WHITESPACE_RE, "\n")
928931
}
929932

930933
fun format(r: Any) = formatter.apply(r, "")

codegen-core/src/test/kotlin/software/amazon/smithy/rust/codegen/core/rustlang/RustWriterTest.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,4 +228,37 @@ class RustWriterTest {
228228
val sut = RustWriter.forModule("src/module_name")
229229
sut.module().shouldBe("module_name")
230230
}
231+
232+
// Regression test for https://github.com/smithy-lang/smithy-rs/issues/4634
233+
@Test
234+
fun `output does not contain trailing whitespace`() {
235+
val sut = RustWriter.root()
236+
// Reproduce the two patterns from the issue:
237+
sut.rustBlock("fn example()") {
238+
// join("\n") separator creates a blank line that gets indented
239+
val writables =
240+
listOf(
241+
writable { rust("let a = 1;") },
242+
writable { rust("let b = 2;") },
243+
)
244+
writables.join("\n")(this)
245+
246+
// Empty string interpolation leaves a blank line that gets indented
247+
val empty = ""
248+
rust(
249+
"""
250+
something
251+
.method()
252+
$empty
253+
.chain()
254+
""",
255+
)
256+
}
257+
val output = sut.toString()
258+
val trailingWhitespaceLines =
259+
output.lines().filter { line ->
260+
line != line.trimEnd()
261+
}
262+
trailingWhitespaceLines shouldBe emptyList()
263+
}
231264
}

0 commit comments

Comments
 (0)