@@ -23,14 +23,17 @@ import software.amazon.smithy.rust.codegen.core.smithy.protocols.restJsonFieldNa
2323import software.amazon.smithy.rust.codegen.core.smithy.transformers.OperationNormalizer
2424import software.amazon.smithy.rust.codegen.core.smithy.transformers.RecursiveShapeBoxer
2525import software.amazon.smithy.rust.codegen.core.testutil.TestWorkspace
26+ import software.amazon.smithy.rust.codegen.core.testutil.TestWriterDelegator
2627import software.amazon.smithy.rust.codegen.core.testutil.asSmithyModel
2728import software.amazon.smithy.rust.codegen.core.testutil.compileAndTest
2829import software.amazon.smithy.rust.codegen.core.testutil.renderWithModelBuilder
30+ import software.amazon.smithy.rust.codegen.core.testutil.rustSettings
2931import software.amazon.smithy.rust.codegen.core.testutil.testCodegenContext
3032import software.amazon.smithy.rust.codegen.core.testutil.testSymbolProvider
3133import software.amazon.smithy.rust.codegen.core.testutil.unitTest
3234import software.amazon.smithy.rust.codegen.core.util.inputShape
3335import software.amazon.smithy.rust.codegen.core.util.lookup
36+ import software.amazon.smithy.rust.codegen.core.util.runCommand
3437
3538class JsonSerializerGeneratorTest {
3639 private val baseModel =
@@ -348,46 +351,37 @@ class JsonSerializerGeneratorTest {
348351 @Test
349352 fun `union with empty struct doesn't cause unused variable warning` () {
350353 // Regression test for https://github.com/smithy-lang/smithy-rs/issues/4308
351- //
352- // This test validates the fix for unused variable warnings in union serialization.
353- // The test model contains an EMPTY STRUCT (not Unit) in a union, which without
354- // the fix would generate: `unused variable: inner` warnings.
355- //
356- // The issue occurs because empty structs still generate union variants like:
357- // EmptyMember(inner) => { ... }
358- // where 'inner' is unused since there's nothing to serialize.
359- //
360- // Test approach: The fix uses '_inner' (underscore prefix) for empty structs,
361- // telling Rust the variable is intentionally unused. This test verifies the
362- // generated code compiles successfully, proving the fix works.
363- val unionWithEmptyStructModel =
354+ val testModel =
364355 """
365356 namespace test
357+ use aws.protocols#restJson1
366358
367- union EncryptionFilter {
368- none: NoneFilter,
369- aes: AesFilter
359+ @restJson1
360+ service TestService {
361+ version: "1.0.0",
362+ operations: [TestOp]
370363 }
371364
372- structure NoneFilter {
373- // Empty structure - this is what causes the unused variable warning!
365+ union TestUnion {
366+ emptyMember: EmptyStruct,
367+ dataMember: String
374368 }
375369
376- structure AesFilter {
377- keyId: String
370+ structure EmptyStruct {
371+ // No members! This causes unused 'inner' variable without the fix
378372 }
379373
380374 @http(uri: "/test", method: "POST")
381375 operation TestOp {
382- input: TestOpInput
376+ input: TestInput
383377 }
384378
385- structure TestOpInput {
386- filter: EncryptionFilter
379+ structure TestInput {
380+ unionField: TestUnion
387381 }
388382 """ .asSmithyModel()
389383
390- val model = OperationNormalizer .transform(unionWithEmptyStructModel )
384+ val model = OperationNormalizer .transform(testModel )
391385 val codegenContext = testCodegenContext(model)
392386 val symbolProvider = codegenContext.symbolProvider
393387 val project = TestWorkspace .testProject(symbolProvider)
@@ -398,30 +392,64 @@ class JsonSerializerGeneratorTest {
398392 HttpTraitHttpBindingResolver (model, ProtocolContentTypes .consistent(" application/json" )),
399393 ::restJsonFieldName,
400394 )
395+ val operationGenerator = jsonSerializer.operationInputSerializer(model.lookup(" test#TestOp" ))
401396
402397 // Render required shapes
403- model.lookup<StructureShape >(" test#NoneFilter" ).renderWithModelBuilder(model, symbolProvider, project)
404- model.lookup<StructureShape >(" test#AesFilter" ).renderWithModelBuilder(model, symbolProvider, project)
405- model.lookup<StructureShape >(" test#TestOpInput" ).renderWithModelBuilder(model, symbolProvider, project)
398+ model.lookup<StructureShape >(" test#EmptyStruct" ).renderWithModelBuilder(model, symbolProvider, project)
399+ model.lookup<OperationShape >(" test#TestOp" ).inputShape(model).renderWithModelBuilder(model, symbolProvider, project)
406400
407- project.moduleFor(model.lookup<UnionShape >(" test#EncryptionFilter " )) {
408- UnionGenerator (model, symbolProvider, this , model.lookup(" test#EncryptionFilter " )).render()
401+ project.moduleFor(model.lookup<UnionShape >(" test#TestUnion " )) {
402+ UnionGenerator (model, symbolProvider, this , model.lookup(" test#TestUnion " )).render()
409403 }
410404
405+ // Generate test that uses the union serialization
411406 project.lib {
412407 unitTest(
413408 " test_union_with_empty_struct" ,
414- """
415- use test_model::{EncryptionFilter, NoneFilter};
409+ ) {
410+ rust(" use test_model::{TestUnion, EmptyStruct};" )
411+ rust(
412+ """
413+ let input = crate::test_input::TestInput::builder()
414+ .union_field(TestUnion::EmptyMember(EmptyStruct {}))
415+ .build()
416+ .unwrap();
417+ """ ,
418+ )
419+ rust(" let _serialized = #T(&input);" , operationGenerator!! )
420+ }
421+ }
416422
417- // This test verifies the generated union serialization code compiles
418- // without unused variable warnings for empty structs
419- let _filter = EncryptionFilter::None(NoneFilter {});
420- """ ,
421- )
423+ // Validate that the fix prevents unused variable warnings:
424+ // - Without fix: generates TestUnion::EmptyMember(inner) => unused variable warning
425+ // - With fix: generates TestUnion::EmptyMember(_inner) => no warning
426+ compileWithWarningsAsErrors(project)
427+ }
428+
429+ private fun compileWithWarningsAsErrors (project : TestWriterDelegator ) {
430+ val stubModel =
431+ """
432+ namespace fake
433+ service Fake {
434+ version: "123"
435+ }
436+ """ .asSmithyModel()
437+
438+ project.finalize(
439+ project.rustSettings(),
440+ stubModel,
441+ manifestCustomizations = emptyMap(),
442+ libRsCustomizations = listOf (),
443+ )
444+
445+ try {
446+ " cargo fmt" .runCommand(project.baseDir)
447+ } catch (e: Exception ) {
448+ // cargo fmt errors are useless, ignore
422449 }
423450
424- // Test compiles successfully - the fix prevents unused variable warnings
425- project.compileAndTest()
451+ // Use RUSTFLAGS to treat unused variable warnings as errors, but allow dead code
452+ val env = mapOf (" RUSTFLAGS" to " -D unused-variables -A dead-code" )
453+ " cargo test" .runCommand(project.baseDir, env)
426454 }
427455}
0 commit comments