From 93a26aa681a4418906d4727790cb8e800759d3a0 Mon Sep 17 00:00:00 2001 From: Phil Carbone Date: Mon, 2 Feb 2026 20:23:51 -0500 Subject: [PATCH 1/2] ci(dependabot): target develop branch for dependency updates (#60) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Configure Dependabot to create PRs against the develop branch instead of main, following GitFlow branching strategy. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 --- .github/dependabot.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2e1753a7..6f627ece 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,9 +3,12 @@ updates: # .NET NuGet dependencies - package-ecosystem: "nuget" directory: "/" + target-branch: "develop" schedule: interval: "weekly" - day: "monday" + day: "friday" + time: "17:00" + timezone: "America/New_York" open-pull-requests-limit: 10 assignees: - "philcarbone" @@ -20,9 +23,12 @@ updates: # GitHub Actions - package-ecosystem: "github-actions" directory: "/" + target-branch: "develop" schedule: interval: "weekly" - day: "monday" + day: "friday" + time: "17:00" + timezone: "America/New_York" open-pull-requests-limit: 5 assignees: - "philcarbone" From 106463814345505b0acd44688d5e8390fd8fbafc Mon Sep 17 00:00:00 2001 From: Phil Carbone Date: Sat, 7 Feb 2026 15:20:28 -0500 Subject: [PATCH 2/2] fix(generators): Resolve duplicate type definitions in MessageJsonContext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When multiple message types share the same simple name in different namespaces (e.g., MyApp.Commands.StartCommand and MyApp.Events.StartCommand), the generator now uses unique identifiers derived from fully qualified names instead of simple names for field and method naming. - Add UniqueIdentifier computed property to JsonMessageTypeInfo - Add ElementUniqueIdentifier computed property to ListTypeInfo - Update snippets to use __UNIQUE_IDENTIFIER__ placeholders - Update generator to use UniqueIdentifier for all naming 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../JsonMessageTypeInfo.cs | 11 +++- src/Whizbang.Generators/ListTypeInfo.cs | 11 +++- .../MessageJsonContextGenerator.cs | 19 +++---- .../Templates/Snippets/JsonContextSnippets.cs | 14 ++--- .../MessageJsonContextGeneratorTests.cs | 51 +++++++++++++++++-- 5 files changed, 85 insertions(+), 21 deletions(-) diff --git a/src/Whizbang.Generators/JsonMessageTypeInfo.cs b/src/Whizbang.Generators/JsonMessageTypeInfo.cs index ffcfca0b..a830171b 100644 --- a/src/Whizbang.Generators/JsonMessageTypeInfo.cs +++ b/src/Whizbang.Generators/JsonMessageTypeInfo.cs @@ -20,7 +20,16 @@ internal sealed record JsonMessageTypeInfo( bool IsSerializable, PropertyInfo[] Properties, bool HasParameterizedConstructor -); +) { + /// + /// Unique identifier derived from fully qualified name, suitable for C# identifiers. + /// Strips "global::" prefix and replaces "." with "_". + /// E.g., "global::MyApp.Commands.StartCommand" becomes "MyApp_Commands_StartCommand". + /// This prevents duplicate field/method names when types have the same SimpleName. + /// + /// tests/Whizbang.Generators.Tests/MessageJsonContextGeneratorTests.cs:Generator_WithSameSimpleNameInDifferentNamespaces_GeneratesUniqueIdentifiersAsync + public string UniqueIdentifier => FullyQualifiedName.Replace("global::", "").Replace(".", "_"); +} /// /// Information about a property for JSON serialization. diff --git a/src/Whizbang.Generators/ListTypeInfo.cs b/src/Whizbang.Generators/ListTypeInfo.cs index 5345f64c..f41c5ad1 100644 --- a/src/Whizbang.Generators/ListTypeInfo.cs +++ b/src/Whizbang.Generators/ListTypeInfo.cs @@ -15,4 +15,13 @@ public sealed record ListTypeInfo( string ListTypeName, string ElementTypeName, string ElementSimpleName -); +) { + /// + /// Unique identifier derived from element type name, suitable for C# identifiers. + /// Strips "global::" prefix and replaces "." with "_". + /// E.g., "global::MyApp.Models.OrderLineItem" becomes "MyApp_Models_OrderLineItem". + /// This prevents duplicate field/method names when element types have the same SimpleName. + /// + /// tests/Whizbang.Generators.Tests/MessageJsonContextGeneratorTests.cs:Generator_WithSameSimpleNameInDifferentNamespaces_GeneratesUniqueIdentifiersAsync + public string ElementUniqueIdentifier => ElementTypeName.Replace("global::", "").Replace(".", "_"); +} diff --git a/src/Whizbang.Generators/MessageJsonContextGenerator.cs b/src/Whizbang.Generators/MessageJsonContextGenerator.cs index 075d4894..19c5fc8a 100644 --- a/src/Whizbang.Generators/MessageJsonContextGenerator.cs +++ b/src/Whizbang.Generators/MessageJsonContextGenerator.cs @@ -50,6 +50,7 @@ public class MessageJsonContextGenerator : IIncrementalGenerator { private const string PLACEHOLDER_MESSAGE_ID = "MessageId"; private const string PLACEHOLDER_FULLY_QUALIFIED_NAME = "__FULLY_QUALIFIED_NAME__"; private const string PLACEHOLDER_SIMPLE_NAME = "__SIMPLE_NAME__"; + private const string PLACEHOLDER_UNIQUE_IDENTIFIER = "__UNIQUE_IDENTIFIER__"; private const string PLACEHOLDER_GLOBAL = "global::"; private const string PLACEHOLDER_INDEX = "__INDEX__"; private const string PLACEHOLDER_PROPERTY_TYPE = "__PROPERTY_TYPE__"; @@ -381,7 +382,7 @@ private static string _generateLazyFields(Assembly assembly, ImmutableArray t.IsCommand || t.IsEvent)) { var field = envelopeFieldSnippet .Replace(PLACEHOLDER_FULLY_QUALIFIED_NAME, type.FullyQualifiedName) - .Replace(PLACEHOLDER_SIMPLE_NAME, type.SimpleName); + .Replace(PLACEHOLDER_UNIQUE_IDENTIFIER, type.UniqueIdentifier); sb.AppendLine(field); } @@ -452,7 +453,7 @@ private static string _generateGetTypeInfo(Assembly assembly, ImmutableArray t.IsCommand || t.IsEvent)) { var check = envelopeCheckSnippet .Replace(PLACEHOLDER_FULLY_QUALIFIED_NAME, type.FullyQualifiedName) - .Replace(PLACEHOLDER_SIMPLE_NAME, type.SimpleName); + .Replace(PLACEHOLDER_UNIQUE_IDENTIFIER, type.UniqueIdentifier); sb.AppendLine(check); sb.AppendLine(); } @@ -473,7 +474,7 @@ private static string _generateGetTypeInfo(Assembly assembly, ImmutableArray Create_{message.SimpleName}(JsonSerializerOptions options) {{"); + sb.AppendLine($"private JsonTypeInfo<{message.FullyQualifiedName}> Create_{message.UniqueIdentifier}(JsonSerializerOptions options) {{"); // Generate properties array sb.AppendLine($" var properties = new JsonPropertyInfo[{message.Properties.Length}];"); @@ -706,7 +707,7 @@ private static string _generateMessageEnvelopeFactories(Assembly assembly, Immut "PARAMETER_INFO_VALUES"); foreach (var message in messages) { - sb.AppendLine($"private JsonTypeInfo> CreateMessageEnvelope_{message.SimpleName}(JsonSerializerOptions options) {{"); + sb.AppendLine($"private JsonTypeInfo> CreateMessageEnvelope_{message.UniqueIdentifier}(JsonSerializerOptions options) {{"); // Generate properties array for MessageEnvelope (MessageId, Payload, Hops) sb.AppendLine(" var properties = new JsonPropertyInfo[3];"); @@ -1018,7 +1019,7 @@ private static string _generateListLazyFields(Assembly assembly, ImmutableArray< foreach (var listType in listTypes) { var field = snippet .Replace("__ELEMENT_TYPE__", listType.ElementTypeName) - .Replace("__ELEMENT_SIMPLE_NAME__", listType.ElementSimpleName); + .Replace("__ELEMENT_UNIQUE_IDENTIFIER__", listType.ElementUniqueIdentifier); sb.AppendLine(field); } @@ -1044,7 +1045,7 @@ private static string _generateListFactories(Assembly assembly, ImmutableArray? ___SIMPLE_NAME__; + private JsonTypeInfo<__FULLY_QUALIFIED_NAME__>? ___UNIQUE_IDENTIFIER__; #endregion #region LAZY_FIELD_MESSAGE_ENVELOPE - private JsonTypeInfo>? _MessageEnvelope___SIMPLE_NAME__; + private JsonTypeInfo>? _MessageEnvelope___UNIQUE_IDENTIFIER__; #endregion #region GET_TYPE_INFO_VALUE_OBJECT @@ -32,13 +32,13 @@ internal class JsonContextSnippets { #region GET_TYPE_INFO_MESSAGE if (type == typeof(__FULLY_QUALIFIED_NAME__)) { - return Create___SIMPLE_NAME__(options); + return Create___UNIQUE_IDENTIFIER__(options); } #endregion #region GET_TYPE_INFO_MESSAGE_ENVELOPE if (type == typeof(MessageEnvelope<__FULLY_QUALIFIED_NAME__>)) { - return CreateMessageEnvelope___SIMPLE_NAME__(options); + return CreateMessageEnvelope___UNIQUE_IDENTIFIER__(options); } #endregion @@ -52,17 +52,17 @@ internal class JsonContextSnippets { #endregion #region LAZY_FIELD_LIST -private JsonTypeInfo>? _List___ELEMENT_SIMPLE_NAME__; +private JsonTypeInfo>? _List___ELEMENT_UNIQUE_IDENTIFIER__; #endregion #region GET_TYPE_INFO_LIST if (type == typeof(global::System.Collections.Generic.List<__ELEMENT_TYPE__>)) { - return CreateList___ELEMENT_SIMPLE_NAME__(options); + return CreateList___ELEMENT_UNIQUE_IDENTIFIER__(options); } #endregion #region LIST_TYPE_FACTORY -private JsonTypeInfo> CreateList___ELEMENT_SIMPLE_NAME__(JsonSerializerOptions options) { +private JsonTypeInfo> CreateList___ELEMENT_UNIQUE_IDENTIFIER__(JsonSerializerOptions options) { var elementInfo = GetOrCreateTypeInfo<__ELEMENT_TYPE__>(options); var collectionInfo = new JsonCollectionInfoValues> { ObjectCreator = static () => new global::System.Collections.Generic.List<__ELEMENT_TYPE__>(), diff --git a/tests/Whizbang.Generators.Tests/MessageJsonContextGeneratorTests.cs b/tests/Whizbang.Generators.Tests/MessageJsonContextGeneratorTests.cs index 13ccdf3b..117637de 100644 --- a/tests/Whizbang.Generators.Tests/MessageJsonContextGeneratorTests.cs +++ b/tests/Whizbang.Generators.Tests/MessageJsonContextGeneratorTests.cs @@ -155,7 +155,8 @@ public record CreateOrder(string OrderId) : ICommand; await Assert.That(code).IsNotNull(); // Should generate specific factory method for MessageEnvelope - await Assert.That(code!).Contains("CreateMessageEnvelope_CreateOrder"); + // Uses unique identifier from fully qualified name: MyApp.Commands.CreateOrder -> MyApp_Commands_CreateOrder + await Assert.That(code!).Contains("CreateMessageEnvelope_MyApp_Commands_CreateOrder"); await Assert.That(code).Contains("MessageEnvelope"); } @@ -612,7 +613,51 @@ public record CreateOrder(string OrderId, List PublicItems) : ICom await Assert.That(code).IsNotNull(); await Assert.That(code!).Contains("CreateOrder"); await Assert.That(code).Contains("PublicDetail"); - // PublicDetail should have factory method - await Assert.That(code).Contains("Create_PublicDetail"); + // PublicDetail should have factory method (uses unique identifier from FQN) + await Assert.That(code).Contains("Create_MyApp_PublicDetail"); + } + + [Test] + [RequiresAssemblyFiles()] + public async Task Generator_WithSameSimpleNameInDifferentNamespaces_GeneratesUniqueIdentifiersAsync() { + // Arrange - Two types with same SimpleName but different namespaces + // This would previously cause duplicate field names (_StartCommand) and factory methods + var source = """ +using Whizbang.Core; + +namespace MyApp.Commands { + public record StartCommand(string Data) : ICommand; +} + +namespace MyApp.Events { + public record StartCommand(string Data) : IEvent; +} +"""; + + // Act + var result = GeneratorTestHelper.RunGenerator(source); + + // Assert - Should not have compilation errors (duplicate identifiers would cause errors) + await Assert.That(result.Diagnostics).DoesNotContain(d => d.Severity == DiagnosticSeverity.Error); + + var code = GeneratorTestHelper.GetGeneratedSource(result, "MessageJsonContext.g.cs"); + await Assert.That(code).IsNotNull(); + + // Both types should be present with fully qualified names + await Assert.That(code!).Contains("global::MyApp.Commands.StartCommand"); + await Assert.That(code).Contains("global::MyApp.Events.StartCommand"); + + // Should have unique field names (not duplicate _StartCommand) + // Uses namespace-qualified identifiers like _MyApp_Commands_StartCommand + await Assert.That(code).Contains("_MyApp_Commands_StartCommand"); + await Assert.That(code).Contains("_MyApp_Events_StartCommand"); + + // Should have unique factory method names + await Assert.That(code).Contains("Create_MyApp_Commands_StartCommand"); + await Assert.That(code).Contains("Create_MyApp_Events_StartCommand"); + + // Should have unique MessageEnvelope factory method names + await Assert.That(code).Contains("CreateMessageEnvelope_MyApp_Commands_StartCommand"); + await Assert.That(code).Contains("CreateMessageEnvelope_MyApp_Events_StartCommand"); } }