Skip to content

Commit

Permalink
## 8.3.0.0 - 2024-10-30
Browse files Browse the repository at this point in the history
### Added
- Possibility to use static method for constructing an object instead of normal constructors.
   - simple usage ```[MakeBuilder(typeof(Entity), staticFactoryMethodName: nameof(Entity.CreateEntity))]```
   - It may be useful when your entity has private constructors and you create it by factory methods.
  • Loading branch information
pmrogala committed Oct 30, 2024
1 parent fe6146b commit eb6125a
Show file tree
Hide file tree
Showing 15 changed files with 214 additions and 104 deletions.
6 changes: 4 additions & 2 deletions Buildenator.Abstraction/MakeBuilderAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ public sealed class MakeBuilderAttribute : Attribute
/// <param name="defaultStaticCreator">The resulting builder will have a special static building method with default parameters. true/false/null</param>
/// <param name="nullableStrategy">Change nullable context behaviour. Use the <see cref="NullableStrategy"/> enum.</param>
/// <param name="implicitCast">Should the builder have implicit cast to the target type.</param>
/// <param name="generateMethodsForUnreachableProperties"></param>
/// <param name="generateMethodsForUnreachableProperties">It will create methods for setting up properties that does not have public setter.</param>
/// <param name="staticFactoryMethodName">if you want to use a static factory method for constructing an entity, you can bring here the name.</param>
public MakeBuilderAttribute(
Type typeForBuilder,
string? buildingMethodsPrefix = "With",
object? defaultStaticCreator = null,
object? nullableStrategy = null,
object? generateMethodsForUnreachableProperties = null,
object? implicitCast = null
object? implicitCast = null,
string? staticFactoryMethodName = null
)
{
}
Expand Down
2 changes: 1 addition & 1 deletion Buildenator/Buildenator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<IncludeBuildOutput>false</IncludeBuildOutput>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<!-- Do not include the generator as a lib dependency -->
<Version>8.2.1.0</Version>
<Version>8.3.0.0</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion Buildenator/BuildersGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
var (fixtureProperties, mockingProperties, builderProperties, typeForBuilder) = properties;
return new BuilderSourceStringGenerator(builderProperties,
new EntityToBuild(typeForBuilder, mockingProperties, fixtureProperties,
builderProperties.NullableStrategy),
builderProperties.NullableStrategy, builderProperties.StaticFactoryMethodName),
fixtureProperties,
mockingProperties);
});
Expand Down
5 changes: 4 additions & 1 deletion Buildenator/Configuration/BuilderProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ public static BuilderProperties Create(
nullableStrategy,
builderAttribute.GenerateMethodsForUnreachableProperties ??
generateMethodsForUnreachableProperties,
builderAttribute.ImplicitCast ?? implicitCast));
builderAttribute.ImplicitCast ?? implicitCast,
builderAttribute.StaticFactoryMethodName));
}

private BuilderProperties(INamespaceOrTypeSymbol builderSymbol, MakeBuilderAttributeInternal attributeData)
Expand All @@ -67,6 +68,7 @@ private BuilderProperties(INamespaceOrTypeSymbol builderSymbol, MakeBuilderAttri
ImplicitCast = attributeData.ImplicitCast ?? false;
ShouldGenerateMethodsForUnreachableProperties = attributeData.GenerateMethodsForUnreachableProperties ?? false;
OriginalLocation = builderSymbol.Locations.First();
StaticFactoryMethodName = attributeData.StaticFactoryMethodName;

if (string.IsNullOrWhiteSpace(BuildingMethodsPrefix))
throw new ArgumentNullException(nameof(attributeData), "Prefix name shouldn't be empty!");
Expand Down Expand Up @@ -117,6 +119,7 @@ when method.Name.StartsWith(BuildingMethodsPrefix)
public bool ShouldGenerateMethodsForUnreachableProperties { get; }
public bool IsBuildMethodOverriden { get; }
public Location OriginalLocation { get; }
public string? StaticFactoryMethodName { get; }

public IReadOnlyDictionary<string, IMethodSymbol> BuildingMethods => _buildingMethods;
public IReadOnlyDictionary<string, IFieldSymbol> Fields => _fields;
Expand Down
3 changes: 3 additions & 0 deletions Buildenator/Configuration/Contract/IEntityToBuild.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ internal interface IEntityToBuild : IAdditionalNamespacesProvider
EntityToBuild.Constructor? ConstructorToBuild { get; }
IEnumerable<BuildenatorDiagnostic> Diagnostics { get; }

string GenerateDefaultBuildEntityString(IEnumerable<ITypedSymbol> parameters, IEnumerable<ITypedSymbol> properties);
string GenerateStaticBuildsCode();
IReadOnlyList<ITypedSymbol> GetAllUniqueReadOnlyPropertiesWithoutConstructorsParametersMatch();
IReadOnlyList<ITypedSymbol> GetAllUniqueSettablePropertiesAndParameters();
(IReadOnlyList<ITypedSymbol> Parameters, IReadOnlyList<ITypedSymbol> Properties) GetParametersAndProperties();
}
119 changes: 109 additions & 10 deletions Buildenator/Configuration/EntityToBuild.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Linq;
using Buildenator.Abstraction;
using Buildenator.Diagnostics;
using System.Text;

namespace Buildenator.Configuration;

Expand All @@ -19,12 +20,14 @@ internal sealed class EntityToBuild : IEntityToBuild
public IReadOnlyList<TypedSymbol> ReadOnlyProperties { get; }
public string[] AdditionalNamespaces { get; }
public IEnumerable<BuildenatorDiagnostic> Diagnostics => _diagnostics;
public NullableStrategy NullableStrategy { get; }

public EntityToBuild(
INamedTypeSymbol typeForBuilder,
IMockingProperties? mockingConfiguration,
IFixtureProperties? fixtureConfiguration,
NullableStrategy nullableStrategy)
NullableStrategy nullableStrategy,
string? staticFactoryMethodName)
{
INamedTypeSymbol? entityToBuildSymbol;
var additionalNamespaces = Enumerable.Empty<string>();
Expand All @@ -46,8 +49,9 @@ public EntityToBuild(
FullNameWithConstraints = entityToBuildSymbol.ToDisplayString(new SymbolDisplayFormat(
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters | SymbolDisplayGenericsOptions.IncludeTypeConstraints | SymbolDisplayGenericsOptions.IncludeVariance));

ConstructorToBuild = Constructor.CreateConstructorOrDefault(entityToBuildSymbol, mockingConfiguration, fixtureConfiguration, nullableStrategy);
ConstructorToBuild = Constructor.CreateConstructorOrDefault(entityToBuildSymbol, mockingConfiguration, fixtureConfiguration, nullableStrategy, staticFactoryMethodName);
(SettableProperties, ReadOnlyProperties) = DividePropertiesBySetability(entityToBuildSymbol, mockingConfiguration, fixtureConfiguration, nullableStrategy);
NullableStrategy = nullableStrategy;
}

public IReadOnlyList<ITypedSymbol> GetAllUniqueSettablePropertiesAndParameters()
Expand Down Expand Up @@ -83,19 +87,93 @@ private static (TypedSymbol[] Settable, TypedSymbol[] ReadOnly) DivideProperties
readOnly.Select(a => new TypedSymbol(a, mockingConfiguration, fixtureConfiguration, nullableStrategy)).ToArray());
}

public string GenerateDefaultBuildEntityString(IEnumerable<ITypedSymbol> parameters, IEnumerable<ITypedSymbol> properties)
{
if (ConstructorToBuild is StaticConstructor staticConstructor)
{
return @$"return {FullName}.{staticConstructor.Name}({parameters.Select(a => a.GenerateFieldValueReturn()).ComaJoin()});";
}
else
{
var propertiesAssignment = properties.Select(property => $"{property.SymbolName} = {property.GenerateFieldValueReturn()}").ComaJoin();
return @$"return new {FullName}({parameters.Select(a => a.GenerateFieldValueReturn()).ComaJoin()})
{{
{(string.IsNullOrEmpty(propertiesAssignment) ? string.Empty : $" {propertiesAssignment}")}
}};";
}
}

public (IReadOnlyList<ITypedSymbol> Parameters, IReadOnlyList<ITypedSymbol> Properties) GetParametersAndProperties()
{
IEnumerable<TypedSymbol> parameters = [];
IEnumerable<TypedSymbol> properties = SettableProperties;
if (ConstructorToBuild is not null)
{
parameters = ConstructorToBuild.Parameters;
properties = properties.Where(x => !ConstructorToBuild.ContainsParameter(x.SymbolName));
}

return (parameters.ToList(), properties.ToList());
}

public string GenerateStaticBuildsCode()
{
if (ConstructorToBuild is null)
return "";

var (parameters, properties) = GetParametersAndProperties();
var moqInit = parameters
.Concat(properties)
.Where(symbol => symbol.IsMockable())
.Select(s => $@" {s.GenerateFieldInitialization()}")
.Aggregate(new StringBuilder(), (builder, s) => builder.AppendLine(s))
.ToString();

var methodParameters = parameters
.Concat(properties)
.Select(s =>
{
var fieldType = s.GenerateFieldType();
return $"{fieldType} {s.UnderScoreName} = default({fieldType})";
}).ComaJoin();
var disableWarning = NullableStrategy == NullableStrategy.Enabled
? "#pragma warning disable CS8625\n"
: string.Empty;
var restoreWarning = NullableStrategy == NullableStrategy.Enabled
? "#pragma warning restore CS8625\n"
: string.Empty;

return $@"{disableWarning} public static {FullName} BuildDefault({methodParameters})
{{
{moqInit}
{GenerateDefaultBuildEntityString(parameters, properties)}
}}
{restoreWarning}";
}

private IReadOnlyList<TypedSymbol>? _uniqueTypedSymbols;
private IReadOnlyList<TypedSymbol>? _uniqueReadOnlyTypedSymbols;
private readonly List<BuildenatorDiagnostic> _diagnostics = [];

internal sealed class Constructor
internal abstract class Constructor
{
public static Constructor? CreateConstructorOrDefault(
INamedTypeSymbol entityToBuildSymbol,
IMockingProperties? mockingConfiguration,
IFixtureProperties? fixtureConfiguration,
NullableStrategy nullableStrategy)
NullableStrategy nullableStrategy,
string? staticFactoryMethodName)
{
var constructors = entityToBuildSymbol.Constructors.Select(a => a).ToArray();
IMethodSymbol[] constructors;
if (staticFactoryMethodName is null)
{
constructors = entityToBuildSymbol.Constructors.Select(a => a).ToArray();
}
else
{
constructors = entityToBuildSymbol.GetMembers(staticFactoryMethodName).OfType<IMethodSymbol>().Where(a => a.IsStatic).ToArray();
}

var onlyPublicConstructors = constructors
.Where(m => m.DeclaredAccessibility == Accessibility.Public || m.DeclaredAccessibility == Accessibility.Internal)
.ToList();
Expand All @@ -105,16 +183,19 @@ internal sealed class Constructor

Dictionary<string, TypedSymbol> parameters = [];

parameters = onlyPublicConstructors
.OrderByDescending(x => x.Parameters.Length)
.First()
var selectedConstructor = onlyPublicConstructors
.OrderByDescending(x => x.Parameters.Length)
.First();
parameters = selectedConstructor
.Parameters
.ToDictionary(x => x.PascalCaseName(), s => new TypedSymbol(s, mockingConfiguration, fixtureConfiguration, nullableStrategy));

return new Constructor(parameters);
return staticFactoryMethodName is null
? new ObjectConstructor(parameters)
: new StaticConstructor(parameters, selectedConstructor.Name);
}

private Constructor(IReadOnlyDictionary<string, TypedSymbol> constructorParameters)
protected Constructor(IReadOnlyDictionary<string, TypedSymbol> constructorParameters)
{
ConstructorParameters = constructorParameters;
}
Expand All @@ -124,4 +205,22 @@ private Constructor(IReadOnlyDictionary<string, TypedSymbol> constructorParamete
public bool ContainsParameter(string parameterName) => ConstructorParameters.ContainsKey(parameterName);
public IEnumerable<TypedSymbol> Parameters => ConstructorParameters.Values;
}

internal sealed class ObjectConstructor : Constructor
{
internal ObjectConstructor(IReadOnlyDictionary<string, TypedSymbol> constructorParameters)
: base(constructorParameters)
{
}
}

internal sealed class StaticConstructor : Constructor
{
internal StaticConstructor(IReadOnlyDictionary<string, TypedSymbol> constructorParameters, string name)
: base(constructorParameters)
{
Name = name;
}
public string Name { get; }
}
}
75 changes: 17 additions & 58 deletions Buildenator/Generators/BuilderSourceStringGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ namespace {_builder.ContainingNamespace}
{_propertiesStringGenerator.GeneratePropertiesCode()}
{GenerateBuildsCode()}
{GenerateBuildManyCode()}
{(_builder.StaticCreator ? GenerateStaticBuildsCode() : string.Empty)}
{(_builder.StaticCreator ? _entity.GenerateStaticBuildsCode() : string.Empty)}
{(_builder.ImplicitCast ? GenerateImplicitCastCode() : string.Empty)}
{GeneratePostBuildMethod()}
}}
Expand Down Expand Up @@ -101,7 +101,7 @@ private string GenerateBuildsCode()
if (_entity.ConstructorToBuild is null || _builder.IsBuildMethodOverriden)
return "";

var (parameters, properties) = GetParametersAndProperties();
var (parameters, properties) = _entity.GetParametersAndProperties();

var disableWarning = _builder.NullableStrategy == NullableStrategy.Enabled
? "#pragma warning disable CS8604\n"
Expand Down Expand Up @@ -129,68 +129,31 @@ private string GenerateBuildManyCode()

}

private string GenerateStaticBuildsCode()
{
if (_entity.ConstructorToBuild is null)
return "";

var (parameters, properties) = GetParametersAndProperties();
var moqInit = parameters
.Concat(properties)
.Where(symbol => symbol.IsMockable())
.Select(s => $@" {s.GenerateFieldInitialization()}")
.Aggregate(new StringBuilder(), (builder, s) => builder.AppendLine(s))
.ToString();

var methodParameters = parameters
.Concat(properties)
.Select(s =>
{
var fieldType = s.GenerateFieldType();
return $"{fieldType} {s.UnderScoreName} = default({fieldType})";
}).ComaJoin();
var disableWarning = _builder.NullableStrategy == NullableStrategy.Enabled
? "#pragma warning disable CS8625\n"
: string.Empty;
var restoreWarning = _builder.NullableStrategy == NullableStrategy.Enabled
? "#pragma warning restore CS8625\n"
: string.Empty;

return $@"{disableWarning} public static {_entity.FullName} BuildDefault({methodParameters})
{{
{moqInit}
{GenerateBuildEntityString(parameters, properties)}
}}
{restoreWarning}";

}

private string GenerateImplicitCastCode()
{
return $@" public static implicit operator {_entity.FullName}({_builder.FullName} builder) => builder.{DefaultConstants.BuildMethodName}();";
}

private (IReadOnlyList<ITypedSymbol> Parameters, IReadOnlyList<ITypedSymbol> Properties) GetParametersAndProperties()
{
IEnumerable<TypedSymbol> parameters = [];
IEnumerable<TypedSymbol> properties = _entity.SettableProperties;
if (_entity.ConstructorToBuild is not null)
{
parameters = _entity.ConstructorToBuild.Parameters;
properties = properties.Where(x => !_entity.ConstructorToBuild.ContainsParameter(x.SymbolName));
}

return (parameters.ToList(), properties.ToList());
}

private string GenerateLazyBuildEntityString(IEnumerable<ITypedSymbol> parameters, IEnumerable<ITypedSymbol> properties)
{
var propertiesAssignment = properties.Select(property => $"{property.SymbolName} = {property.GenerateLazyFieldValueReturn()}").ComaJoin();
return @$"var result = new {_entity.FullName}({parameters.Select(symbol => symbol.GenerateLazyFieldValueReturn()).ComaJoin()})
var onlyConstructorString = string.Empty;
if (_entity.ConstructorToBuild is EntityToBuild.StaticConstructor staticConstructor)
{
onlyConstructorString = @$"var result = {_entity.FullName}.{staticConstructor.Name}({parameters.Select(symbol => symbol.GenerateLazyFieldValueReturn()).ComaJoin()});
";
}
else
{
onlyConstructorString = @$"var result = new {_entity.FullName}({parameters.Select(symbol => symbol.GenerateLazyFieldValueReturn()).ComaJoin()})
{{
{(string.IsNullOrEmpty(propertiesAssignment) ? string.Empty : $" {propertiesAssignment}")}
}};
{(_builder.ShouldGenerateMethodsForUnreachableProperties ? GenerateUnreachableProperties() : "")}
";
}

return onlyConstructorString
+ $@"{(_builder.ShouldGenerateMethodsForUnreachableProperties ? GenerateUnreachableProperties() : "")}
{DefaultConstants.PostBuildMethodName}(result);
return result;";

Expand All @@ -210,11 +173,7 @@ string GenerateUnreachableProperties()

private string GenerateBuildEntityString(IEnumerable<ITypedSymbol> parameters, IEnumerable<ITypedSymbol> properties)
{
var propertiesAssignment = properties.Select(property => $"{property.SymbolName} = {property.GenerateFieldValueReturn()}").ComaJoin();
return @$"return new {_entity.FullName}({parameters.Select(a => a.GenerateFieldValueReturn()).ComaJoin()})
{{
{(string.IsNullOrEmpty(propertiesAssignment) ? string.Empty : $" {propertiesAssignment}")}
}};";
return _entity.GenerateDefaultBuildEntityString(parameters, properties);
}

private static readonly string AutoGenerationComment = @$"
Expand Down
Loading

0 comments on commit eb6125a

Please sign in to comment.