diff --git a/src/FluentBuilderGenerator/FileGenerators/FluentBuilderClassesGenerator.cs b/src/FluentBuilderGenerator/FileGenerators/FluentBuilderClassesGenerator.cs index 943233e..ce640b1 100644 --- a/src/FluentBuilderGenerator/FileGenerators/FluentBuilderClassesGenerator.cs +++ b/src/FluentBuilderGenerator/FileGenerators/FluentBuilderClassesGenerator.cs @@ -436,10 +436,17 @@ private static (bool IsPrimaryConstructor, IReadOnlyList p.IsPublicSettable()).Select(p => new PropertyOrParameterSymbol(p, p.Type, p.IsInitOnly(), p.IsRequired))) { - if (propertiesPublicSettable.All(p => p.Name != property.Name)) + var existingIndex = propertiesPublicSettable.FindIndex(p => string.Equals(p.Name, property.Name, StringComparison.OrdinalIgnoreCase)); + if (existingIndex < 0) { propertiesPublicSettable.Add(property); } + else if (propertiesPublicSettable[existingIndex].PropertyType == PropertyType.Parameter) + { + // Replace the constructor parameter entry with the property entry so that + // _isSet tracking and property-assignment logic are correctly generated. + propertiesPublicSettable[existingIndex] = property; + } } var propertiesPrivateSettable = accessibility != FluentBuilderAccessibility.PublicAndPrivate ? [] : @@ -517,8 +524,7 @@ private static string GenerateSeveralMethods(FluentData fluentData, ClassSymbol if (isPrimaryConstructor) { - var parameters = propertiesPublicSettable - .Where(p => p.PropertyType == PropertyType.Parameter) + var parameters = publicConstructors[0].Parameters .Select(p => $"_{p.Name.ToCamelCase()}.Value"); output.AppendLine(20, $"else {{ instance = new {className}({string.Join(", ", parameters)}); }}"); diff --git a/tests/FluentBuilderGeneratorTests/DTO/ClassWithConstructorParamMatchingProperty.cs b/tests/FluentBuilderGeneratorTests/DTO/ClassWithConstructorParamMatchingProperty.cs new file mode 100644 index 0000000..576edcf --- /dev/null +++ b/tests/FluentBuilderGeneratorTests/DTO/ClassWithConstructorParamMatchingProperty.cs @@ -0,0 +1,19 @@ +namespace FluentBuilderGeneratorTests.DTO; + +public enum StageType +{ + Build, + Test +} + +public class ClassWithConstructorParamMatchingProperty +{ + public ClassWithConstructorParamMatchingProperty(StageType type) + { + Type = type; + } + + public StageType Type { get; set; } + + public string Name { get; set; } = string.Empty; +} diff --git a/tests/FluentBuilderGeneratorTests/FluentBuilderSourceGeneratorTests.cs b/tests/FluentBuilderGeneratorTests/FluentBuilderSourceGeneratorTests.cs index 39af48e..a59323a 100644 --- a/tests/FluentBuilderGeneratorTests/FluentBuilderSourceGeneratorTests.cs +++ b/tests/FluentBuilderGeneratorTests/FluentBuilderSourceGeneratorTests.cs @@ -907,4 +907,42 @@ public void GenerateFiles_ForRecordWithPrimaryConstructor_Should_GenerateCorrect data.Should().BeEquivalentTo(new { Normal = "normal", Data = "t50" }); } + + [Fact] + public void GenerateFiles_ForClassWithConstructorParamMatchingProperty_ShouldNotDuplicate() + { + // Arrange (Issue #76: constructor parameter name matching a property name should not create duplicate members) + var path = "./DTO/ClassWithConstructorParamMatchingProperty.cs"; + var sourceFile = new SourceFile + { + Path = path, + Text = File.ReadAllText(path), + AttributeToAddToClass = "FluentBuilder.AutoGenerateBuilder" + }; + + // Act + var result = _sut.Execute(Namespace, [sourceFile]); + + // Assert + result.Valid.Should().BeTrue(); + result.Files.Should().HaveCount(NumFiles); + result.Files.Should().NotContain(r => r.Path.EndsWith("Error.g.cs")); + + var fileResult = result.Files[NumFiles - 1]; + var filename = Path.GetFileName(fileResult.Path); + + if (Write) File.WriteAllText($"../../../DTO/{filename}", fileResult.Text); + + // Verify the generated code does not contain duplicate '_type' fields + var generatedCode = fileResult.Text; + var typeFieldCount = System.Text.RegularExpressions.Regex.Matches(generatedCode, @"private Lazy<.*> _type\b").Count; + typeFieldCount.Should().Be(1, "the '_type' field should only be declared once"); + + var withTypeMethodCount = System.Text.RegularExpressions.Regex.Matches(generatedCode, @"public .* WithType\(").Count; + withTypeMethodCount.Should().Be(2, "WithType should only have two overloads (value and Func)"); + + // Verify constructor/property wiring in generated code. + generatedCode.Should().Contain("else { instance = new ClassWithConstructorParamMatchingProperty(_type.Value); }"); + generatedCode.Should().Contain("if (_typeIsSet) { Instance.Value.Type = _type.Value; }"); + } } \ No newline at end of file