From 044b9d3c6e1e5fe09e30954b4207f5db69d640f6 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Mon, 26 Oct 2026 20:22:07 -0400 Subject: [PATCH] fix(generator): validate alias capture member names --- CHANGELOG.md | 2 + packages/ack_generator/CHANGELOG.md | 2 + .../analyzer/schema_model_graph_builder.dart | 20 +++---- .../generated_names_contract_test.dart | 55 +++++++++++++++++++ 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc5ad2c0..44acdae0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline ### Fixed +* Reject generator aliases whose final passthrough policy makes an inherited + field collide with the generated additional-properties member. * Preserve round trips for discriminated map codecs with strict output schemas, including values used as another codec's output. * Accept contextual Dart member names and deeply snapshot JSON-owned generated diff --git a/packages/ack_generator/CHANGELOG.md b/packages/ack_generator/CHANGELOG.md index 988971ed..21ab80dc 100644 --- a/packages/ack_generator/CHANGELOG.md +++ b/packages/ack_generator/CHANGELOG.md @@ -7,6 +7,8 @@ ### Fixed +* Validate inherited field names against an alias's final capture policy before + generation, rejecting collisions with the additional-properties member. * Reject unproven object-shape changes inside preserved boundaries, including aliases and fluent codecs, rather than copying opaque values as JSON. * Preserve native codec boundary types when inferring `Ack.preserveBoundary` diff --git a/packages/ack_generator/lib/src/analyzer/schema_model_graph_builder.dart b/packages/ack_generator/lib/src/analyzer/schema_model_graph_builder.dart index e43d59a0..bfe0cddc 100644 --- a/packages/ack_generator/lib/src/analyzer/schema_model_graph_builder.dart +++ b/packages/ack_generator/lib/src/analyzer/schema_model_graph_builder.dart @@ -388,16 +388,6 @@ final class SchemaModelGraphBuilder extends ModelGraphBuilderBase { element: declaration.element, ); } - final problem = ackSchemaFirstFieldNameProblem( - jsonKey, - capturesExtras: additionalProperties, - ); - if (problem != null) { - throw InvalidGenerationSource( - '$path.$jsonKey $problem', - element: declaration.element, - ); - } final fieldPath = '$path.$jsonKey'; final fieldChain = parseSchemaChain(entry.value); final fieldMetadata = await _schemaMetadata( @@ -1239,6 +1229,16 @@ final class SchemaModelGraphBuilder extends ModelGraphBuilderBase { } if (node is AckObjectModelNode) { for (final field in node.fields) { + final problem = ackSchemaFirstFieldNameProblem( + field.dartName, + capturesExtras: node.additionalProperties, + ); + if (problem != null) { + throw InvalidGenerationSource( + '${node.id.declarationName}.${field.dartName} $problem', + element: declaration.element, + ); + } checkMember(field.dartName); } if (node.discriminatorKey case final discriminator?) { diff --git a/packages/ack_generator/test/integration/generated_names_contract_test.dart b/packages/ack_generator/test/integration/generated_names_contract_test.dart index 26b2b3f8..f510cd46 100644 --- a/packages/ack_generator/test/integration/generated_names_contract_test.dart +++ b/packages/ack_generator/test/integration/generated_names_contract_test.dart @@ -68,6 +68,61 @@ final aliasSchema = originalSchema; ); }); + for (final alias in [ + 'final openSchema = baseSchema.passthrough();', + 'final openSchema = baseSchema.copyWith(additionalProperties: true);', + 'ObjectSchema get openSchema => baseSchema.passthrough();', + '''final middleSchema = baseSchema; +@Schemable() +final openSchema = middleSchema.passthrough();''', + ]) { + test('rejects inherited capture-member collisions: $alias', () async { + await _expectFailure( + ''' +$_head +@Schemable() +final baseSchema = Ack.object({'additionalProperties': Ack.string()}); +@Schemable() +$alias +''', + ['openSchema.additionalProperties', 'additional-properties member'], + ); + }); + } + + for (final overrides in [ + '.passthrough().strict()', + '.copyWith(additionalProperties: true).copyWith(additionalProperties: false)', + ]) { + test('permits inherited additionalProperties after $overrides', () async { + final readerWriter = TestReaderWriter(rootPackage: 'test_pkg'); + await readerWriter.testing.loadIsolateSources(); + await testBuilder( + ackModelBuilder(BuilderOptions.empty), + { + 'test_pkg|lib/models.dart': + ''' +$_head +@Schemable() +final baseSchema = Ack.object({'additionalProperties': Ack.string()}); +@Schemable() +final aliasSchema = baseSchema$overrides; +''', + }, + readerWriter: readerWriter, + outputs: { + 'test_pkg|lib/models.g.dart': decodedMatches( + allOf( + contains('class Alias'), + contains('final String additionalProperties;'), + isNot(contains('Map additionalProperties')), + ), + ), + }, + ); + }); + } + for (final discriminator in ['Pet', 'Cat']) { test('rejects discriminator matching class $discriminator', () async { await _expectFailure(