diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml index 693c054..e3cd28d 100644 --- a/.github/workflows/pr-gate.yml +++ b/.github/workflows/pr-gate.yml @@ -11,10 +11,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Setup .NET Core 8 + - name: Setup .NET Core 10 uses: actions/setup-dotnet@v1 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - name: Install dependencies run: dotnet restore - name: Build diff --git a/.github/workflows/publish-nugets.yml b/.github/workflows/publish-nugets.yml index 46900cb..e4e35d7 100644 --- a/.github/workflows/publish-nugets.yml +++ b/.github/workflows/publish-nugets.yml @@ -36,10 +36,10 @@ jobs: echo Version number extracted from ${GITHUB_REF} is invalid exit 1 - uses: actions/checkout@v2 - - name: Setup .NET Core 8 + - name: Setup .NET Core 10 uses: actions/setup-dotnet@v1 with: - dotnet-version: 8.0.x + dotnet-version: 10.0.x - name: Install dependencies run: dotnet restore - name: Build diff --git a/Buildenator.sln b/Buildenator.sln index 2460065..bc4fa4d 100644 --- a/Buildenator.sln +++ b/Buildenator.sln @@ -40,8 +40,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Buildenator.IntegrationTest EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Buildenator.IntegrationTests.SharedEntities", "Tests\Buildenator.IntegrationTests.SharedEntities\Buildenator.IntegrationTests.SharedEntities.csproj", "{A9E79A1B-8E38-4815-AC1A-129CCE999BE1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Buildenator.UnitTests", "Tests\Buildenator.UnitTests\Buildenator.UnitTests.csproj", "{635AFFCA-8ACF-4F92-924C-FB7463D17C3C}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Buildenator.IntegrationTests.SourceNullable", "Tests\Buildenator.IntegrationTests.SourceNullable\Buildenator.IntegrationTests.SourceNullable.csproj", "{5F93F84C-D4C2-43DD-B979-083BD7951FF7}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Buildenator.IntegrationTests.SharedEntitiesNullable", "Tests\Buildenator.IntegrationTests.SharedEntitiesNullable\Buildenator.IntegrationTests.SharedEntitiesNullable.csproj", "{0AA5BA5D-7119-4E80-8943-F29EA495F752}" @@ -96,10 +94,6 @@ Global {A9E79A1B-8E38-4815-AC1A-129CCE999BE1}.Debug|Any CPU.Build.0 = Debug|Any CPU {A9E79A1B-8E38-4815-AC1A-129CCE999BE1}.Release|Any CPU.ActiveCfg = Release|Any CPU {A9E79A1B-8E38-4815-AC1A-129CCE999BE1}.Release|Any CPU.Build.0 = Release|Any CPU - {635AFFCA-8ACF-4F92-924C-FB7463D17C3C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {635AFFCA-8ACF-4F92-924C-FB7463D17C3C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {635AFFCA-8ACF-4F92-924C-FB7463D17C3C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {635AFFCA-8ACF-4F92-924C-FB7463D17C3C}.Release|Any CPU.Build.0 = Release|Any CPU {5F93F84C-D4C2-43DD-B979-083BD7951FF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5F93F84C-D4C2-43DD-B979-083BD7951FF7}.Debug|Any CPU.Build.0 = Debug|Any CPU {5F93F84C-D4C2-43DD-B979-083BD7951FF7}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -120,7 +114,6 @@ Global {7023762A-B29A-4A4E-888E-5D6A54E72D4E} = {34D14563-E89B-4337-B751-C50D9149C827} {63A6F9E6-56D5-4AF9-8725-54E2EC09E7F4} = {34D14563-E89B-4337-B751-C50D9149C827} {A9E79A1B-8E38-4815-AC1A-129CCE999BE1} = {34D14563-E89B-4337-B751-C50D9149C827} - {635AFFCA-8ACF-4F92-924C-FB7463D17C3C} = {34D14563-E89B-4337-B751-C50D9149C827} {5F93F84C-D4C2-43DD-B979-083BD7951FF7} = {34D14563-E89B-4337-B751-C50D9149C827} {0AA5BA5D-7119-4E80-8943-F29EA495F752} = {34D14563-E89B-4337-B751-C50D9149C827} EndGlobalSection diff --git a/Buildenator/AssemblyPropertiesWrapper.cs b/Buildenator/AssemblyPropertiesWrapper.cs new file mode 100644 index 0000000..f3201a9 --- /dev/null +++ b/Buildenator/AssemblyPropertiesWrapper.cs @@ -0,0 +1,43 @@ +using Microsoft.CodeAnalysis; +#if DEBUG +#endif +using System.Linq; +using System.Collections.Immutable; +using System; + +namespace Buildenator; + +internal sealed class AssemblyPropertiesWrapper( + ImmutableArray? globalFixtureProperties, + ImmutableArray? mockingConfigurationBuilder, + ImmutableArray? globalBuilderProperties) : IEquatable +{ + public readonly ImmutableArray? GlobalFixtureProperties = globalFixtureProperties; + public readonly ImmutableArray? MockingConfigurationBuilder = mockingConfigurationBuilder; + public readonly ImmutableArray? GlobalBuilderProperties = globalBuilderProperties; + + public override bool Equals(object? obj) => obj is AssemblyPropertiesWrapper other && Equals(other); + + public override int GetHashCode() + { + int hashCode = 874553126; + hashCode = hashCode * -1521134295 + GlobalFixtureProperties?.GetHashCode() ?? 0; + hashCode = hashCode * -1521134295 + MockingConfigurationBuilder?.GetHashCode() ?? 0; + hashCode = hashCode * -1521134295 + GlobalBuilderProperties?.GetHashCode() ?? 0; + return hashCode; + } + + public void Deconstruct(out ImmutableArray? globalFixtureProperties, out ImmutableArray? mockingConfigurationBuilder, out ImmutableArray? globalBuilderProperties) + { + globalFixtureProperties = GlobalFixtureProperties; + mockingConfigurationBuilder = MockingConfigurationBuilder; + globalBuilderProperties = GlobalBuilderProperties; + } + + public bool Equals(AssemblyPropertiesWrapper other) + { + return (GlobalFixtureProperties?.SequenceEqual(other.GlobalFixtureProperties ?? []) ?? (GlobalFixtureProperties is null && other.GlobalFixtureProperties is null)) + && (MockingConfigurationBuilder?.SequenceEqual(other.MockingConfigurationBuilder ?? []) ?? (MockingConfigurationBuilder is null && other.MockingConfigurationBuilder is null)) + && (GlobalBuilderProperties?.SequenceEqual(other.GlobalBuilderProperties ?? []) ?? (GlobalBuilderProperties is null && other.GlobalBuilderProperties is null)); + } +} \ No newline at end of file diff --git a/Buildenator/Buildenator.csproj b/Buildenator/Buildenator.csproj index 6bd7832..6a6914b 100644 --- a/Buildenator/Buildenator.csproj +++ b/Buildenator/Buildenator.csproj @@ -23,11 +23,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Buildenator/BuilderAttributesWrapper.cs b/Buildenator/BuilderAttributesWrapper.cs new file mode 100644 index 0000000..37c8196 --- /dev/null +++ b/Buildenator/BuilderAttributesWrapper.cs @@ -0,0 +1,72 @@ +using Microsoft.CodeAnalysis; +using System.Linq; +using System.Collections.Immutable; + +namespace Buildenator; + +internal sealed class BuilderAttributesWrapper( + INamedTypeSymbol BuilderSymbol, + MakeBuilderAttributeInternal BuilderAttribute, + ImmutableArray? MockingAttribute, + ImmutableArray? FixtureAttribute) : System.IEquatable +{ + public INamedTypeSymbol BuilderSymbol { get; } = BuilderSymbol; + public MakeBuilderAttributeInternal BuilderAttribute { get; } = BuilderAttribute; + public ImmutableArray? MockingAttribute { get; } = MockingAttribute; + public ImmutableArray? FixtureAttribute { get; } = FixtureAttribute; + + public bool Equals(BuilderAttributesWrapper other) + { + return + BuilderSymbol.Equals(other.BuilderSymbol, SymbolEqualityComparer.Default) + && BuilderAttribute.Equals(other.BuilderAttribute) + && (MockingAttribute?.SequenceEqual(other.MockingAttribute ?? []) ?? (MockingAttribute is null && other.MockingAttribute is null)) + && (FixtureAttribute?.SequenceEqual(other.FixtureAttribute ?? []) ?? (FixtureAttribute is null && other.FixtureAttribute is null)); + } + + public void Deconstruct(out INamedTypeSymbol BuilderSymbol, out MakeBuilderAttributeInternal BuilderAttribute, out ImmutableArray? MockingAttribute, out ImmutableArray? FixtureAttribute) + { + BuilderSymbol = this.BuilderSymbol; + BuilderAttribute = this.BuilderAttribute; + MockingAttribute = this.MockingAttribute; + FixtureAttribute = this.FixtureAttribute; + } + + public override bool Equals(object? obj) => obj is BuilderAttributesWrapper other && Equals(other); + + public override int GetHashCode() + { + unchecked + { + var hash = 17; + hash = (hash * 31) + SymbolEqualityComparer.Default.GetHashCode(BuilderSymbol); + hash = (hash * 31) + BuilderAttribute.GetHashCode(); + + if (MockingAttribute != null) + { + foreach (var item in MockingAttribute) + { + hash = (hash * 31) + item.GetHashCode(); + } + } + else + { + hash *= 31; + } + + if (FixtureAttribute != null) + { + foreach (var item in FixtureAttribute) + { + hash = (hash * 31) + item.GetHashCode(); + } + } + else + { + hash *= 31; + } + + return hash; + } + } +} \ No newline at end of file diff --git a/Buildenator/BuildersGenerator.cs b/Buildenator/BuildersGenerator.cs index 22c51e9..afec1e4 100644 --- a/Buildenator/BuildersGenerator.cs +++ b/Buildenator/BuildersGenerator.cs @@ -36,13 +36,13 @@ public void Initialize(IncrementalGeneratorInitializationContext context) transform: static (ctx, _) => { var attributes = ctx.TargetSymbol.GetAttributes(); - return ( - BuilderSymbol: (INamedTypeSymbol)ctx.TargetSymbol, - BuilderAttribute: new MakeBuilderAttributeInternal( + return new BuilderAttributesWrapper( + (INamedTypeSymbol)ctx.TargetSymbol, + new MakeBuilderAttributeInternal( attributes.Single( attributeData => attributeData.AttributeClass?.Name == nameof(MakeBuilderAttribute))), - MockingAttribute: GetMockingConfigurationOrDefault(attributes), - FixtureAttribute: GetLocalFixturePropertiesOrDefault(attributes) + GetMockingConfigurationOrDefault(attributes), + GetLocalFixturePropertiesOrDefault(attributes) ); } ); @@ -57,25 +57,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .Select(static (c, _) => c.Assembly) .Select(static (assembly, _) => assembly.GetAttributes()) .Select(static (attributes, _) => - ( - Mocking: attributes.Where(x => + new AssemblyPropertiesWrapper( + attributes.Where(x => + x.AttributeClass.HasNameOrBaseClassHas(nameof(FixtureConfigurationAttribute))) + .FirstOrDefault()?.ConstructorArguments, + attributes.Where(x => x.AttributeClass.HasNameOrBaseClassHas(nameof(MockingConfigurationAttribute))) - .FirstOrDefault(), - Builder: attributes.Where(x => + .FirstOrDefault()?.ConstructorArguments, + attributes.Where(x => x.AttributeClass.HasNameOrBaseClassHas(nameof(BuildenatorConfigurationAttribute))) - .FirstOrDefault(), - Fixture: attributes.Where(x => - x.AttributeClass.HasNameOrBaseClassHas(nameof(FixtureConfigurationAttribute))) - .FirstOrDefault() - )) - .Select(static (assembly, _) => - { - var globalFixtureProperties = assembly.Fixture?.ConstructorArguments; - var mockingConfigurationBuilder = assembly.Mocking?.ConstructorArguments; - var globalBuilderProperties = assembly.Builder?.ConstructorArguments; - - return (globalFixtureProperties, mockingConfigurationBuilder, globalBuilderProperties); - }); + .FirstOrDefault()?.ConstructorArguments + )); // Collect all builders and their entity types to create a mapping // This allows us to find child builders for properties when useChildBuilders is enabled @@ -121,15 +113,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context) var builderProperties = BuilderProperties.Create(builderNamedTypeSymbol, builderAttribute, globalBuilderProperties, nullableOptions.AnnotationsEnabled()); - return (fixtureProperties, mockingConfiguration: mockingProperties, builderProperties, - builderAttribute.TypeForBuilder, builderMappings); - }) - .Select(static (properties, _) => - { - var (fixtureProperties, mockingProperties, builderProperties, typeForBuilder, builderMappings) = properties; return new BuilderSourceStringGenerator(builderProperties, new EntityToBuild( - typeForBuilder, + builderAttribute.TypeForBuilder, mockingProperties, fixtureProperties, builderProperties.NullableStrategy, @@ -166,9 +152,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) }); } - private static (SyntaxTree SyntaxTree, SemanticModel SemanticModel) SelectSyntaxTreeAndSemanticModel((SyntaxTree SyntaxTree, Compilation Compilation) tuple, CancellationToken _) - => (tuple.SyntaxTree, SemanticModel: tuple.Compilation.GetSemanticModel(tuple.SyntaxTree)); - private static ImmutableArray? GetLocalFixturePropertiesOrDefault(ImmutableArray attributeData) { var attribute = attributeData.SingleOrDefault(x => x.AttributeClass.HasNameOrBaseClassHas(nameof(FixtureConfigurationAttribute))); @@ -179,4 +162,4 @@ private static (SyntaxTree SyntaxTree, SemanticModel SemanticModel) SelectSyntax attributeData .SingleOrDefault(x => x.AttributeClass.HasNameOrBaseClassHas(nameof(MockingConfigurationAttribute))) ?.ConstructorArguments; -} \ No newline at end of file +} diff --git a/Buildenator/Generators/BuilderSourceStringGenerator.cs b/Buildenator/Generators/BuilderSourceStringGenerator.cs index 2f6b456..8e9d79c 100644 --- a/Buildenator/Generators/BuilderSourceStringGenerator.cs +++ b/Buildenator/Generators/BuilderSourceStringGenerator.cs @@ -1,5 +1,4 @@ using Buildenator.Abstraction; -using Buildenator.CodeAnalysis; using Buildenator.Configuration.Contract; using System.Collections.Generic; using Buildenator.Configuration; diff --git a/Buildenator/Generators/PropertiesStringGenerator.cs b/Buildenator/Generators/PropertiesStringGenerator.cs index 083177c..9d028d0 100644 --- a/Buildenator/Generators/PropertiesStringGenerator.cs +++ b/Buildenator/Generators/PropertiesStringGenerator.cs @@ -94,12 +94,16 @@ bool IsNotYetDeclaredAddToMethod(ITypedSymbol x) if (collectionMetadata == null) return true; - // Check if any method has a matching params array parameter + // Check if any method has a matching params parameter (array or IEnumerable) return !methods.Any(method => method.Parameters.Length == 1 && method.Parameters[0].IsParams && - method.Parameters[0].Type is IArrayTypeSymbol arrayType && - arrayType.ElementType.Name == collectionMetadata.ElementTypeName); + ((method.Parameters[0].Type is IArrayTypeSymbol arrayType && + arrayType.ElementType.Name == collectionMetadata.ElementTypeName) || + (method.Parameters[0].Type is INamedTypeSymbol namedType && + namedType.Name == "IEnumerable" && + namedType.TypeArguments.Length == 1 && + namedType.TypeArguments[0].Name == collectionMetadata.ElementTypeName))); } bool IsCollectionProperty(ITypedSymbol x) => x.GetCollectionMetadata() != null && !x.IsMockable(); @@ -158,10 +162,14 @@ bool IsNotYetDeclaredChildBuilderAddToMethod(ITypedSymbol x) if (!_builder.BuildingMethods.TryGetValue(CreateAddToMethodName(x), out var methods)) return true; - // Check if any method has a Func parameter + // Check if any method has a Func parameter (either params Func<>[] or params IEnumerable>) return !methods.Any(method => method.Parameters.Length == 1 && - method.Parameters[0].Type.Name.StartsWith("Func")); + (method.Parameters[0].Type.Name.StartsWith("Func") || + (method.Parameters[0].Type is INamedTypeSymbol namedType2 && + namedType2.Name == "IEnumerable" && + namedType2.TypeArguments.Length == 1 && + namedType2.TypeArguments[0].Name.StartsWith("Func")))); } } @@ -198,6 +206,12 @@ private static string GenerateValueAssignment(ITypedSymbol typedSymbol) private string CreateMethodName(ITypedSymbol property) => $"{_builder.BuildingMethodsPrefix}{property.SymbolPascalName}"; + private static string FieldHasValue(string fieldName) + => $"{fieldName} != null && {fieldName}.HasValue && {fieldName}.Value.Object != null"; + + private static string ParamsIEnumerable(string elementType, string paramName = "items") + => $"params System.Collections.Generic.IEnumerable<{elementType}> {paramName}"; + private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol) { var collectionMetadata = typedSymbol.GetCollectionMetadata(); @@ -207,16 +221,29 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol) var elementTypeName = collectionMetadata.ElementTypeDisplayName; var methodName = CreateAddToMethodName(typedSymbol); var fieldName = typedSymbol.UnderScoreName; - - // For concrete dictionary types, use Dictionary's indexer for adding + if (collectionMetadata is ConcreteDictionaryMetadata concreteDictMetadata) - { - var keyTypeName = concreteDictMetadata.KeyTypeDisplayName; - var valueTypeName = concreteDictMetadata.ValueTypeDisplayName; - return $@"public {_builder.FullName} {methodName}(params System.Collections.Generic.KeyValuePair<{keyTypeName}, {valueTypeName}>[] items) + return GenerateConcreteDictionaryAddTo(methodName, fieldName, typedSymbol, concreteDictMetadata); + + if (collectionMetadata is InterfaceDictionaryMetadata interfaceDictMetadata) + return GenerateInterfaceDictionaryAddTo(methodName, fieldName, typedSymbol, interfaceDictMetadata); + + if (collectionMetadata is ArrayCollectionMetadata or ConcreteCollectionMetadata) + return GenerateArrayOrConcreteCollectionAddTo(methodName, fieldName, typedSymbol, collectionMetadata, elementTypeName); + + // Interface collection types: use List and AddRange + return GenerateInterfaceCollectionAddTo(methodName, fieldName, typedSymbol, elementTypeName); + } + + private string CreateAddToMethodName(ITypedSymbol property) => $"AddTo{property.SymbolPascalName}"; + + private string GenerateConcreteDictionaryAddTo(string methodName, string fieldName, ITypedSymbol typedSymbol, ConcreteDictionaryMetadata meta) + { + var kvpType = $"System.Collections.Generic.KeyValuePair<{meta.KeyTypeDisplayName}, {meta.ValueTypeDisplayName}>"; + return $@"public {_builder.FullName} {methodName}({ParamsIEnumerable(kvpType)}) {{ {typedSymbol.TypeFullName} dictionary; - if ({fieldName} != null && {fieldName}.HasValue && {fieldName}.Value.Object != null) + if ({FieldHasValue(fieldName)}) {{ dictionary = {fieldName}.Value.Object; }} @@ -233,17 +260,15 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol) {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>(dictionary); return this; }}"; - } - - // For interface dictionary types, create a new Dictionary and use indexer - if (collectionMetadata is InterfaceDictionaryMetadata interfaceDictMetadata) - { - var keyTypeName = interfaceDictMetadata.KeyTypeDisplayName; - var valueTypeName = interfaceDictMetadata.ValueTypeDisplayName; - var dictionaryType = $"System.Collections.Generic.Dictionary<{keyTypeName}, {valueTypeName}>"; - return $@"public {_builder.FullName} {methodName}(params System.Collections.Generic.KeyValuePair<{keyTypeName}, {valueTypeName}>[] items) + } + + private string GenerateInterfaceDictionaryAddTo(string methodName, string fieldName, ITypedSymbol typedSymbol, InterfaceDictionaryMetadata meta) + { + var kvpType = $"System.Collections.Generic.KeyValuePair<{meta.KeyTypeDisplayName}, {meta.ValueTypeDisplayName}>"; + var dictionaryType = $"System.Collections.Generic.Dictionary<{meta.KeyTypeDisplayName}, {meta.ValueTypeDisplayName}>"; + return $@"public {_builder.FullName} {methodName}({ParamsIEnumerable(kvpType)}) {{ - var dictionary = {fieldName} != null && {fieldName}.HasValue && {fieldName}.Value.Object != null + var dictionary = {FieldHasValue(fieldName)} ? new {dictionaryType}({fieldName}.Value.Object) : new {dictionaryType}(); foreach (var item in items) @@ -253,28 +278,25 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol) {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>(dictionary); return this; }}"; - } - - // For array types and concrete collection types, use similar pattern - if (collectionMetadata is ArrayCollectionMetadata or ConcreteCollectionMetadata) - { - var isArray = collectionMetadata is ArrayCollectionMetadata; - var collectionVarName = isArray ? "array" : "collection"; - var collectionTypeName = isArray ? $"{elementTypeName}[]" : typedSymbol.TypeFullName; - - var addItemsCode = isArray - ? $@"if ({fieldName} != null && {fieldName}.HasValue && {fieldName}.Value.Object != null) + } + + private string GenerateArrayOrConcreteCollectionAddTo(string methodName, string fieldName, ITypedSymbol typedSymbol, CollectionMetadata collectionMetadata, string elementTypeName) + { + var isArray = collectionMetadata is ArrayCollectionMetadata; + var collectionVarName = isArray ? "array" : "collection"; + var collectionTypeName = isArray ? $"{elementTypeName}[]" : typedSymbol.TypeFullName; + + var addItemsCode = isArray + ? $@"var itemsArray = System.Linq.Enumerable.ToArray(items); + if ({FieldHasValue(fieldName)}) {{ - var existingArray = {fieldName}.Value.Object; - {collectionVarName} = new {elementTypeName}[existingArray.Length + items.Length]; - System.Array.Copy(existingArray, 0, {collectionVarName}, 0, existingArray.Length); - System.Array.Copy(items, 0, {collectionVarName}, existingArray.Length, items.Length); + {collectionVarName} = System.Linq.Enumerable.ToArray(System.Linq.Enumerable.Concat({fieldName}.Value.Object, itemsArray)); }} else {{ - {collectionVarName} = items; + {collectionVarName} = itemsArray; }}" - : $@"if ({fieldName} != null && {fieldName}.HasValue && {fieldName}.Value.Object != null) + : $@"if ({FieldHasValue(fieldName)}) {{ {collectionVarName} = {fieldName}.Value.Object; }} @@ -287,8 +309,8 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol) {{ {collectionVarName}.Add(item); }}"; - - return $@"public {_builder.FullName} {methodName}(params {elementTypeName}[] items) + + return $@"public {_builder.FullName} {methodName}({ParamsIEnumerable(elementTypeName)}) {{ {collectionTypeName} {collectionVarName}; {addItemsCode} @@ -296,12 +318,13 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol) {fieldName} = new {DefaultConstants.NullBox}<{typedSymbol.TypeFullName}>({collectionVarName}); return this; }}"; - } - - // For interface types, use List and AddRange - return $@"public {_builder.FullName} {methodName}(params {elementTypeName}[] items) + } + + private string GenerateInterfaceCollectionAddTo(string methodName, string fieldName, ITypedSymbol typedSymbol, string elementTypeName) + { + return $@"public {_builder.FullName} {methodName}({ParamsIEnumerable(elementTypeName)}) {{ - var list = {fieldName} != null && {fieldName}.HasValue && {fieldName}.Value.Object != null + var list = {FieldHasValue(fieldName)} ? new System.Collections.Generic.List<{elementTypeName}>({fieldName}.Value.Object) : new System.Collections.Generic.List<{elementTypeName}>(); list.AddRange(items); @@ -310,8 +333,6 @@ private string GenerateAddToMethodDefinition(ITypedSymbol typedSymbol) }}"; } - private string CreateAddToMethodName(ITypedSymbol property) => $"AddTo{property.SymbolPascalName}"; - /// /// Gets the builder name for a property's type. /// Returns null if no child builder exists for the property's type. @@ -354,7 +375,7 @@ private string GenerateChildBuilderAddToMethodDefinition(ITypedSymbol typedSymbo var elementTypeName = collectionMetadata.ElementTypeDisplayName; var methodName = CreateAddToMethodName(typedSymbol); var fieldName = typedSymbol.UnderScoreName; - + var funcType = $"System.Func<{childBuilderName}, {childBuilderName}>"; // For array types and concrete collection types, use similar pattern if (collectionMetadata is ArrayCollectionMetadata or ConcreteCollectionMetadata) { @@ -364,28 +385,26 @@ private string GenerateChildBuilderAddToMethodDefinition(ITypedSymbol typedSymbo // Build child items first - for arrays we need to know the count upfront var buildItemsCode = isArray - ? $@"var newItems = new {elementTypeName}[configures.Length]; - for (int i = 0; i < configures.Length; i++) + ? $@"var newItemsList = new System.Collections.Generic.List<{elementTypeName}>(); + foreach (var configure in configures) {{ var childBuilder = new {childBuilderName}(); - childBuilder = configures[i](childBuilder); - newItems[i] = childBuilder.Build(); - }}" + childBuilder = configure(childBuilder); + newItemsList.Add(childBuilder.Build()); + }} + var newItems = newItemsList.ToArray();" : ""; var addItemsCode = isArray - ? $@"if ({fieldName} != null && {fieldName}.HasValue && {fieldName}.Value.Object != null) + ? $@"if ({FieldHasValue(fieldName)}) {{ - var existingArray = {fieldName}.Value.Object; - {collectionVarName} = new {elementTypeName}[existingArray.Length + newItems.Length]; - System.Array.Copy(existingArray, 0, {collectionVarName}, 0, existingArray.Length); - System.Array.Copy(newItems, 0, {collectionVarName}, existingArray.Length, newItems.Length); + {collectionVarName} = System.Linq.Enumerable.ToArray(System.Linq.Enumerable.Concat({fieldName}.Value.Object, newItems)); }} else {{ {collectionVarName} = newItems; }}" - : $@"if ({fieldName} != null && {fieldName}.HasValue && {fieldName}.Value.Object != null) + : $@"if ({FieldHasValue(fieldName)}) {{ {collectionVarName} = {fieldName}.Value.Object; }} @@ -409,8 +428,8 @@ private string GenerateChildBuilderAddToMethodDefinition(ITypedSymbol typedSymbo {addItemsCode}" : $@"{collectionTypeName} {collectionVarName}; {addItemsCode}"; - - return $@"public {_builder.FullName} {methodName}(params System.Func<{childBuilderName}, {childBuilderName}>[] configures) + + return $@"public {_builder.FullName} {methodName}({ParamsIEnumerable(funcType, "configures")}) {{ {methodBody} @@ -420,9 +439,9 @@ private string GenerateChildBuilderAddToMethodDefinition(ITypedSymbol typedSymbo } // For interface collection types, use List - return $@"public {_builder.FullName} {methodName}(params System.Func<{childBuilderName}, {childBuilderName}>[] configures) + return $@"public {_builder.FullName} {methodName}({ParamsIEnumerable(funcType, "configures")}) {{ - var list = {fieldName} != null && {fieldName}.HasValue && {fieldName}.Value.Object != null + var list = {FieldHasValue(fieldName)} ? new System.Collections.Generic.List<{elementTypeName}>({fieldName}.Value.Object) : new System.Collections.Generic.List<{elementTypeName}>(); diff --git a/Buildenator/MakeBuilderAttributeInternal.cs b/Buildenator/MakeBuilderAttributeInternal.cs index 39900c3..4b188b2 100644 --- a/Buildenator/MakeBuilderAttributeInternal.cs +++ b/Buildenator/MakeBuilderAttributeInternal.cs @@ -3,7 +3,7 @@ namespace Buildenator; -internal readonly struct MakeBuilderAttributeInternal( +internal sealed class MakeBuilderAttributeInternal( INamedTypeSymbol typeForBuilder, string? buildingMethodsPrefix, bool? staticCreator, @@ -14,7 +14,7 @@ internal readonly struct MakeBuilderAttributeInternal( bool? generateStaticPropertyForBuilderCreation, bool? initializeCollectionsWithEmpty, bool? useChildBuilders, - bool? builderConstructorMandatoryParameters) + bool? builderConstructorMandatoryParameters) : System.IEquatable { public MakeBuilderAttributeInternal(AttributeData attribute) @@ -47,4 +47,42 @@ attribute.ConstructorArguments[3].Value is null public bool? UseChildBuilders { get; } = useChildBuilders; public bool? BuilderConstructorMandatoryParameters { get; } = builderConstructorMandatoryParameters; internal string? StaticFactoryMethodName { get; } = staticFactoryMethodName; + + public bool Equals(MakeBuilderAttributeInternal other) + { + return + TypeForBuilder.Equals(other.TypeForBuilder, SymbolEqualityComparer.Default) + && BuildingMethodsPrefix == other.BuildingMethodsPrefix + && GenerateDefaultBuildMethod == other.GenerateDefaultBuildMethod + && ImplicitCast == other.ImplicitCast + && NullableStrategy == other.NullableStrategy + && GenerateMethodsForUnreachableProperties == other.GenerateMethodsForUnreachableProperties + && GenerateStaticPropertyForBuilderCreation == other.GenerateStaticPropertyForBuilderCreation + && InitializeCollectionsWithEmpty == other.InitializeCollectionsWithEmpty + && UseChildBuilders == other.UseChildBuilders + && BuilderConstructorMandatoryParameters == other.BuilderConstructorMandatoryParameters + && StaticFactoryMethodName == other.StaticFactoryMethodName; + } + + public override bool Equals(object? obj) => obj is MakeBuilderAttributeInternal other && Equals(other); + + public override int GetHashCode() + { + unchecked + { + var hash = 17; + hash = (hash * 31) + SymbolEqualityComparer.Default.GetHashCode(TypeForBuilder); + hash = (hash * 31) + (BuildingMethodsPrefix?.GetHashCode() ?? 0); + hash = (hash * 31) + (GenerateDefaultBuildMethod?.GetHashCode() ?? 0); + hash = (hash * 31) + (ImplicitCast?.GetHashCode() ?? 0); + hash = (hash * 31) + (NullableStrategy?.GetHashCode() ?? 0); + hash = (hash * 31) + (GenerateMethodsForUnreachableProperties?.GetHashCode() ?? 0); + hash = (hash * 31) + (GenerateStaticPropertyForBuilderCreation?.GetHashCode() ?? 0); + hash = (hash * 31) + (InitializeCollectionsWithEmpty?.GetHashCode() ?? 0); + hash = (hash * 31) + (UseChildBuilders?.GetHashCode() ?? 0); + hash = (hash * 31) + (BuilderConstructorMandatoryParameters?.GetHashCode() ?? 0); + hash = (hash * 31) + (StaticFactoryMethodName?.GetHashCode() ?? 0); + return hash; + } + } } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5469e8c..4295b03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 10.8.0.0 - 2026-3-8 + +### Added +Support for .net 10 functionalities + +### Changed +- **`AddTo` generated methods now use `params IEnumerable`** instead of `params T[]`, leveraging the .NET 10 `params` collections feature. This applies to all generated `AddTo` methods for collection, array, and dictionary properties, as well as child-builder `AddTo` overloads. + ## 8.8.0.0 - 2026-3-6 ### Added diff --git a/Samples/SampleTestProject/SampleTestProject.csproj b/Samples/SampleTestProject/SampleTestProject.csproj index 4bbdca4..208d712 100644 --- a/Samples/SampleTestProject/SampleTestProject.csproj +++ b/Samples/SampleTestProject/SampleTestProject.csproj @@ -1,24 +1,24 @@  - net8 + net10.0 false - - - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/Tests/Buildenator.Benchmarks/Buildenator.Benchmarks.csproj b/Tests/Buildenator.Benchmarks/Buildenator.Benchmarks.csproj index 62311bc..ae6779a 100644 --- a/Tests/Buildenator.Benchmarks/Buildenator.Benchmarks.csproj +++ b/Tests/Buildenator.Benchmarks/Buildenator.Benchmarks.csproj @@ -2,14 +2,15 @@ Exe - net8 + net10.0 false + true - - + + diff --git a/Tests/Buildenator.Benchmarks/EmptySourceGenerator.cs b/Tests/Buildenator.Benchmarks/EmptySourceGenerator.cs index 198c09a..d4cfe9c 100644 --- a/Tests/Buildenator.Benchmarks/EmptySourceGenerator.cs +++ b/Tests/Buildenator.Benchmarks/EmptySourceGenerator.cs @@ -2,13 +2,11 @@ namespace Buildenator.Benchmarks; -internal class EmptySourceGenerator : ISourceGenerator +[Generator] +internal class EmptySourceGenerator : IIncrementalGenerator { - public void Execute(GeneratorExecutionContext context) + public void Initialize(IncrementalGeneratorInitializationContext initContext) { - } - public void Initialize(GeneratorInitializationContext context) - { } } \ No newline at end of file diff --git a/Tests/Buildenator.IntegrationTests.Source/Buildenator.IntegrationTests.Source.csproj b/Tests/Buildenator.IntegrationTests.Source/Buildenator.IntegrationTests.Source.csproj index 17b1510..1d48722 100644 --- a/Tests/Buildenator.IntegrationTests.Source/Buildenator.IntegrationTests.Source.csproj +++ b/Tests/Buildenator.IntegrationTests.Source/Buildenator.IntegrationTests.Source.csproj @@ -1,14 +1,14 @@  - net8 + net10.0 false - + diff --git a/Tests/Buildenator.IntegrationTests.SourceNullable/Buildenator.IntegrationTests.SourceNullable.csproj b/Tests/Buildenator.IntegrationTests.SourceNullable/Buildenator.IntegrationTests.SourceNullable.csproj index c5f4bab..e0c26a6 100644 --- a/Tests/Buildenator.IntegrationTests.SourceNullable/Buildenator.IntegrationTests.SourceNullable.csproj +++ b/Tests/Buildenator.IntegrationTests.SourceNullable/Buildenator.IntegrationTests.SourceNullable.csproj @@ -1,14 +1,14 @@ - net8 + net10.0 false enable - - + + diff --git a/Tests/Buildenator.IntegrationTests.SourceWithoutAssemblyInfo/Buildenator.IntegrationTests.SourceWithoutAssemblyInfo.csproj b/Tests/Buildenator.IntegrationTests.SourceWithoutAssemblyInfo/Buildenator.IntegrationTests.SourceWithoutAssemblyInfo.csproj index 9713ed7..c4f96f8 100644 --- a/Tests/Buildenator.IntegrationTests.SourceWithoutAssemblyInfo/Buildenator.IntegrationTests.SourceWithoutAssemblyInfo.csproj +++ b/Tests/Buildenator.IntegrationTests.SourceWithoutAssemblyInfo/Buildenator.IntegrationTests.SourceWithoutAssemblyInfo.csproj @@ -1,7 +1,7 @@ - netstandard2.0 + net10.0 false diff --git a/Tests/Buildenator.IntegrationTests/Buildenator.IntegrationTests.csproj b/Tests/Buildenator.IntegrationTests/Buildenator.IntegrationTests.csproj index 801610c..55cd1be 100644 --- a/Tests/Buildenator.IntegrationTests/Buildenator.IntegrationTests.csproj +++ b/Tests/Buildenator.IntegrationTests/Buildenator.IntegrationTests.csproj @@ -1,7 +1,7 @@  - net8 + net10.0 enable false @@ -9,18 +9,18 @@ - - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Tests/Buildenator.IntegrationTests/BuildersGeneratorNullableTests.cs b/Tests/Buildenator.IntegrationTests/BuildersGeneratorNullableTests.cs index 738eb6d..7455795 100644 --- a/Tests/Buildenator.IntegrationTests/BuildersGeneratorNullableTests.cs +++ b/Tests/Buildenator.IntegrationTests/BuildersGeneratorNullableTests.cs @@ -1,12 +1,10 @@ -using AutoFixture.Xunit2; using Buildenator.IntegrationTests.SharedEntitiesNullable; using Buildenator.IntegrationTests.SourceNullable.Builders; using Buildenator.IntegrationTests.SharedEntitiesNullable.DifferentNamespace; using FluentAssertions; -using System.Collections.Generic; -using System.Linq; using Xunit; using PostBuildEntityBuilder = Buildenator.IntegrationTests.SourceNullable.Builders.PostBuildEntityBuilder; +using AutoFixture.Xunit3; namespace Buildenator.IntegrationTests { @@ -145,7 +143,7 @@ public void BuildersGenerator_TwoLevelsInheritance_AllFieldsCreated( .Build(); - typeof(GrandchildEntityBuilder).Should().HaveMethod(nameof(ChildEntityBuilder.WithProtectedProperty), new[] { typeof(List) }); + typeof(GrandchildEntityBuilder).Should().HaveMethod(nameof(ChildEntityBuilder.WithProtectedProperty), [typeof(List)]); result.Should().BeEquivalentTo(grandchildEntity); result.GetPrivateField().Should().BeEquivalentTo(grandchildEntity.GetPrivateField()); result.GetProtectedProperty().Should().BeEquivalentTo(grandchildEntity.GetProtectedProperty()); @@ -167,7 +165,7 @@ public void BuildersGenerator_CustomMethods_CustomMethodsAreCalled( .WithInterfaceType(mock => mock.Setup(x => x.Property).Returns(interfaceProperty)) .Build(); - typeof(EntityBuilderWithCustomMethods).Should().HaveMethod(nameof(ChildEntityBuilder.WithProtectedProperty), new[] { typeof(List) }) + typeof(EntityBuilderWithCustomMethods).Should().HaveMethod(nameof(ChildEntityBuilder.WithProtectedProperty), [typeof(List)]) .Which.Should().HaveAccessModifier(FluentAssertions.Common.CSharpAccessModifier.Private); result.PropertyIntGetter.Should().Be(grandchildEntity.PropertyIntGetter / 2); result.PropertyGetter.Should().Be(grandchildEntity.PropertyGetter + "custom"); diff --git a/Tests/Buildenator.IntegrationTests/BuildersGeneratorTests.cs b/Tests/Buildenator.IntegrationTests/BuildersGeneratorTests.cs index ffab40d..1567b12 100644 --- a/Tests/Buildenator.IntegrationTests/BuildersGeneratorTests.cs +++ b/Tests/Buildenator.IntegrationTests/BuildersGeneratorTests.cs @@ -1,4 +1,4 @@ -using AutoFixture.Xunit2; +using AutoFixture.Xunit3; using Buildenator.IntegrationTests.SharedEntities; using Buildenator.IntegrationTests.Source.Builders; using Buildenator.IntegrationTests.SharedEntities.DifferentNamespace; @@ -1640,4 +1640,63 @@ public void BuildersGenerator_UseChildBuildersFalse_CanStillBuildWithDirectValue _ = result.Child.Should().Be(childEntity); _ = result.ParentValue.Should().Be(parentValue); } + + // ===== Tests for params IEnumerable signature ===== + // These tests verify that the new params IEnumerable signature accepts pre-created collections directly + + [Theory] + [AutoData] + public void BuildersGenerator_CollectionProperty_AddToMethodShouldAcceptPreCreatedIEnumerable(string item1, string item2, string item3) + { + // Verify that params IEnumerable accepts a pre-created collection directly (not just spread args) + IEnumerable items = new List { item1, item2, item3 }; + + var result = EntityWithCollectionAndAddMethodBuilder.EntityWithCollectionAndAddMethod + .AddToEnumerableItems(items) + .Build(); + + _ = result.EnumerableItems.Should().HaveCount(3); + _ = result.EnumerableItems.Should().ContainInOrder(item1, item2, item3); + } + + [Theory] + [AutoData] + public void BuildersGenerator_DictionaryProperty_AddToMethodShouldAcceptPreCreatedIEnumerableOfKvp(string key1, int value1, string key2, int value2) + { + // Verify that params IEnumerable> accepts a pre-created sequence directly + IEnumerable> items = new[] + { + new KeyValuePair(key1, value1), + new KeyValuePair(key2, value2) + }; + + var result = EntityWithDictionaryBuilder.EntityWithDictionary + .AddToScores(items) + .Build(); + + _ = result.Scores.Should().HaveCount(2); + _ = result.Scores[key1].Should().Be(value1); + _ = result.Scores[key2].Should().Be(value2); + } + + [Theory] + [AutoData] + public void BuildersGenerator_UseChildBuildersWithCollection_AddToMethodShouldAcceptPreCreatedIEnumerableOfFuncs( + string childName1, int childValue1, string childName2, int childValue2) + { + // Verify that params IEnumerable> accepts a pre-created array of configurators + var configurators = new Func[] + { + child => child.WithName(childName1).WithValue(childValue1), + child => child.WithName(childName2).WithValue(childValue2) + }; + + var result = ParentWithChildCollectionEntityBuilder.ParentWithChildCollectionEntity + .AddToChildren(configurators) + .Build(); + + _ = result.Children.Should().HaveCount(2); + _ = result.Children[0].Name.Should().Be(childName1); + _ = result.Children[1].Name.Should().Be(childName2); + } } diff --git a/Tests/Buildenator.IntegrationTests/CustomAutoDataAttribute.cs b/Tests/Buildenator.IntegrationTests/CustomAutoDataAttribute.cs index 1571808..5733200 100644 --- a/Tests/Buildenator.IntegrationTests/CustomAutoDataAttribute.cs +++ b/Tests/Buildenator.IntegrationTests/CustomAutoDataAttribute.cs @@ -1,6 +1,6 @@ using AutoFixture; using AutoFixture.AutoMoq; -using AutoFixture.Xunit2; +using AutoFixture.Xunit3; namespace Buildenator.IntegrationTests; diff --git a/Tests/Buildenator.UnitTests/AutoMoqDataAttribute.cs b/Tests/Buildenator.UnitTests/AutoMoqDataAttribute.cs deleted file mode 100644 index 121d238..0000000 --- a/Tests/Buildenator.UnitTests/AutoMoqDataAttribute.cs +++ /dev/null @@ -1,10 +0,0 @@ -using AutoFixture.Xunit2; - -namespace Buildenator.UnitTests; - -public class AutoMoqDataAttribute : AutoDataAttribute -{ - public AutoMoqDataAttribute() : base(MoqFixture.Create) - { - } -} \ No newline at end of file diff --git a/Tests/Buildenator.UnitTests/Buildenator.UnitTests.csproj b/Tests/Buildenator.UnitTests/Buildenator.UnitTests.csproj deleted file mode 100644 index f37ab1b..0000000 --- a/Tests/Buildenator.UnitTests/Buildenator.UnitTests.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - - net8 - enable - latest - false - - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/Tests/Buildenator.UnitTests/Extensions/EnumerableExtensionsTests.cs b/Tests/Buildenator.UnitTests/Extensions/EnumerableExtensionsTests.cs deleted file mode 100644 index 8f347b3..0000000 --- a/Tests/Buildenator.UnitTests/Extensions/EnumerableExtensionsTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Linq; -using AutoFixture; -using Buildenator.Extensions; -using FluentAssertions; -using Xunit; - -namespace Buildenator.UnitTests.Extensions; - -public class EnumerableExtensionsTests -{ - [Fact] - public void Split_ShouldReturnLeftAndRightLists() - { - // Arrange - var fixture = new Fixture(); - var source = fixture.CreateMany(10).ToList(); - var expectedLeft = source.Where(x => x % 2 == 0).ToList(); - var expectedRight = source.Where(x => x % 2 != 0).ToList(); - - // Act - var result = source.AsEnumerable().Split(x => x % 2 == 0); - - // Assert - _ = result.Left.Should().BeEquivalentTo(expectedLeft); - _ = result.Right.Should().BeEquivalentTo(expectedRight); - } - - [Fact] - public void ToLists_ShouldReturnLists() - { - // Arrange - var fixture = new Fixture(); - var source = fixture.CreateMany(10).ToList(); - var expectedLeft = source.Where(x => x % 2 == 0).ToList(); - var expectedRight = source.Where(x => x % 2 != 0).ToList(); - var input = (expectedLeft.AsEnumerable(), expectedRight.AsEnumerable()); - - // Act - var result = input.ToLists(); - - // Assert - _ = result.Left.Should().BeEquivalentTo(expectedLeft); - _ = result.Right.Should().BeEquivalentTo(expectedRight); - } -} \ No newline at end of file diff --git a/Tests/Buildenator.UnitTests/MoqFixture.cs b/Tests/Buildenator.UnitTests/MoqFixture.cs deleted file mode 100644 index df45443..0000000 --- a/Tests/Buildenator.UnitTests/MoqFixture.cs +++ /dev/null @@ -1,12 +0,0 @@ -using AutoFixture; -using AutoFixture.AutoMoq; - -namespace Buildenator.UnitTests; - -public static class MoqFixture -{ - public static IFixture Create() => new Fixture().Customize(new AutoMoqCustomization - { - GenerateDelegates = true - }); -} \ No newline at end of file diff --git a/global.json b/global.json index b76a0b8..8e7309f 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.x" + "version": "10.0.x" } } \ No newline at end of file