Skip to content

Commit

Permalink
## 8.2.0 - 2024-10-23
Browse files Browse the repository at this point in the history
### Added
- Diagnostics for
  - replacing the default Build method
  - replacing the default constructor
  - a builder without the build method error
- Possibility to replace default Build method
  • Loading branch information
pmrogala committed Oct 24, 2024
1 parent 5e43c6c commit bba5a3e
Show file tree
Hide file tree
Showing 14 changed files with 251 additions and 53 deletions.
6 changes: 5 additions & 1 deletion Buildenator/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@
### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|-------
BDN001 | Buildenator | Error | BuildersGenerator
BDN001 | Buildenator | Error | BuildersGenerator
BDN002 | Buildenator | Error | BuildersGenerator
BDN003 | Buildenator | Info | BuildenatorDiagnosticDescriptors
BDN004 | Buildenator | Info | BuildenatorDiagnosticDescriptors
BDN005 | Buildenator | Info | BuildenatorDiagnosticDescriptors
1 change: 1 addition & 0 deletions Buildenator/Buildenator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<IncludeBuildOutput>false</IncludeBuildOutput>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<!-- Do not include the generator as a lib dependency -->
<Version>8.2.0</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
23 changes: 12 additions & 11 deletions Buildenator/BuildersGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Buildenator.Extensions;
using System.Collections.Immutable;
using System.Threading;
using Buildenator.Diagnostics;

[assembly: InternalsVisibleTo("Buildenator.UnitTests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
Expand Down Expand Up @@ -46,8 +47,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
=> (symbolAndAttributesTuple.Symbol,
Attribute: symbolAndAttributesTuple.Attributes.SingleOrDefault(attributeData =>
attributeData.AttributeClass?.Name == nameof(MakeBuilderAttribute))))
.Where(static tuple => tuple.Attribute is not null)
.Select(static (tuple, _) => (tuple.Symbol, Attribute: new MakeBuilderAttributeInternal(tuple.Attribute!)));
.Where(static tuple => tuple.Attribute is not null /* remember about the bang operator in the next line when removing this condition */)
.Select(static (tuple, _) => (BuilderSymbol: tuple.Symbol, Attribute: new MakeBuilderAttributeInternal(tuple.Attribute!)));

var classSymbols = symbolsAndAttributes
.Where(tuple => !tuple.Attribute.TypeForBuilder.IsAbstract);
Expand Down Expand Up @@ -119,22 +120,25 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
{
productionContext.AddCsSourceFile(generator.FileName,
SourceText.From(generator.CreateBuilderCode(), Encoding.UTF8));
foreach(var diagnostic in generator.Diagnostics)
{
productionContext.ReportDiagnostic(diagnostic);
}
});


var abstractClassSymbols = symbolsAndAttributes
.Where(tuple => tuple.Attribute.TypeForBuilder.IsAbstract)
.Select((tuple, _) => (tuple.Symbol, tuple.Attribute.TypeForBuilder));
.Select((tuple, _) => (tuple.BuilderSymbol, tuple.Attribute.TypeForBuilder));

context.RegisterSourceOutput(abstractClassSymbols, (productionContext, tuple)
=>
{
productionContext.ReportDiagnostic(
Diagnostic.Create(AbstractDiagnostic,
tuple.Symbol.Locations.First(),
tuple.TypeForBuilder.Name
)
);
new BuildenatorDiagnostic(BuildenatorDiagnosticDescriptors.AbstractDiagnostic,
tuple.BuilderSymbol.Locations.First(),
tuple.TypeForBuilder.Name)
);
});
}

Expand All @@ -151,7 +155,4 @@ private static (SyntaxTree SyntaxTree, SemanticModel SemanticModel) SelectSyntax
attributeData
.SingleOrDefault(x => x.AttributeClass.HasNameOrBaseClassHas(nameof(MockingConfigurationAttribute)))
?.ConstructorArguments;

private static readonly DiagnosticDescriptor AbstractDiagnostic = new("BDN001", "Cannot generate a builder for an abstract class", "Cannot generate a builder for the {0} abstract class", "Buildenator", DiagnosticSeverity.Error, true);

}
22 changes: 20 additions & 2 deletions Buildenator/Configuration/BuilderProperties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using Buildenator.Extensions;
using System.Linq;
using Buildenator.Diagnostics;

namespace Buildenator.Configuration;

internal readonly struct BuilderProperties : IBuilderProperties
{
private readonly Dictionary<string, IMethodSymbol> _buildingMethods;
private readonly Dictionary<string, IFieldSymbol> _fields;

private readonly List<BuildenatorDiagnostic> _diagnostics = [];

public static BuilderProperties Create(INamespaceOrTypeSymbol builderSymbol,
MakeBuilderAttributeInternal builderAttribute, ImmutableArray<TypedConstant>? globalAttributes)
{
Expand Down Expand Up @@ -52,6 +55,7 @@ private BuilderProperties(INamespaceOrTypeSymbol builderSymbol, MakeBuilderAttri
StaticCreator = attributeData.DefaultStaticCreator ?? true;
ImplicitCast = attributeData.ImplicitCast ?? false;
ShouldGenerateMethodsForUnreachableProperties = attributeData.GenerateMethodsForUnreachableProperties ?? false;
OriginalLocation = builderSymbol.Locations.First();

if (string.IsNullOrWhiteSpace(BuildingMethodsPrefix))
throw new ArgumentNullException(nameof(attributeData), "Prefix name shouldn't be empty!");
Expand All @@ -63,14 +67,25 @@ private BuilderProperties(INamespaceOrTypeSymbol builderSymbol, MakeBuilderAttri
{
switch (member)
{
case IMethodSymbol { MethodKind: MethodKind.Ordinary } method when method.Name.StartsWith(BuildingMethodsPrefix):
case IMethodSymbol { MethodKind: MethodKind.Ordinary } method
when method.Name.StartsWith(BuildingMethodsPrefix)
&& method.Name != DefaultConstants.BuildMethodName:
_buildingMethods.Add(method.Name, method);
break;
case IMethodSymbol { MethodKind: MethodKind.Ordinary, Name: DefaultConstants.PostBuildMethodName }:
IsPostBuildMethodOverriden = true;
break;
case IMethodSymbol { MethodKind: MethodKind.Ordinary, Name: DefaultConstants.BuildMethodName, Parameters.Length: 0 }:
IsBuildMethodOverriden = true;
_diagnostics.Add(new BuildenatorDiagnostic(
BuildenatorDiagnosticDescriptors.BuildMethodOverridenDiagnostic,
OriginalLocation));
break;
case IMethodSymbol { MethodKind: MethodKind.Constructor, Parameters.Length: 0, IsImplicitlyDeclared: false }:
IsDefaultConstructorOverriden = true;
_diagnostics.Add(new BuildenatorDiagnostic(
BuildenatorDiagnosticDescriptors.DefaultConstructorOverridenDiagnostic,
OriginalLocation));
break;
case IFieldSymbol field:
_fields.Add(field.Name, field);
Expand All @@ -89,8 +104,11 @@ private BuilderProperties(INamespaceOrTypeSymbol builderSymbol, MakeBuilderAttri
public bool IsPostBuildMethodOverriden { get; }
public bool IsDefaultConstructorOverriden { get; }
public bool ShouldGenerateMethodsForUnreachableProperties { get; }
public bool IsBuildMethodOverriden { get; }
public Location OriginalLocation { get; }

public IReadOnlyDictionary<string, IMethodSymbol> BuildingMethods => _buildingMethods;
public IReadOnlyDictionary<string, IFieldSymbol> Fields => _fields;

public IEnumerable<BuildenatorDiagnostic> Diagnostics => _diagnostics;
}
4 changes: 4 additions & 0 deletions Buildenator/Configuration/Contract/IBuilderProperties.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Buildenator.Abstraction;
using Buildenator.Diagnostics;
using Microsoft.CodeAnalysis;
using System.Collections.Generic;

Expand All @@ -18,4 +19,7 @@ internal interface IBuilderProperties
bool IsPostBuildMethodOverriden { get; }
bool IsDefaultConstructorOverriden { get; }
bool ShouldGenerateMethodsForUnreachableProperties { get; }
Location OriginalLocation { get; }
bool IsBuildMethodOverriden { get; }
IEnumerable<BuildenatorDiagnostic> Diagnostics { get; }
}
4 changes: 3 additions & 1 deletion Buildenator/Configuration/Contract/IEntityToBuild.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Buildenator.CodeAnalysis;
using Buildenator.Diagnostics;
using Buildenator.Generators;
using System.Collections.Generic;

Expand All @@ -11,7 +12,8 @@ internal interface IEntityToBuild : IAdditionalNamespacesProvider
string Name { get; }
IReadOnlyList<TypedSymbol> SettableProperties { get; }
IReadOnlyList<TypedSymbol> ReadOnlyProperties { get; }
EntityToBuild.Constructor ConstructorToBuild { get; }
EntityToBuild.Constructor? ConstructorToBuild { get; }
IEnumerable<BuildenatorDiagnostic> Diagnostics { get; }

IReadOnlyList<ITypedSymbol> GetAllUniqueReadOnlyPropertiesWithoutConstructorsParametersMatch();
IReadOnlyList<ITypedSymbol> GetAllUniqueSettablePropertiesAndParameters();
Expand Down
1 change: 1 addition & 0 deletions Buildenator/Configuration/DefaultConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ internal static class DefaultConstants
public const string ValueLiteral = "value";
public const string NullBox = "NullBox";
public const string FixtureLiteral = "_fixture";
public const string BuildMethodName = "Build";
}
47 changes: 27 additions & 20 deletions Buildenator/Configuration/EntityToBuild.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using Buildenator.Abstraction;
using System.Diagnostics;
using System.Reflection.Metadata;
using Buildenator.Diagnostics;

namespace Buildenator.Configuration;

Expand All @@ -15,10 +14,11 @@ internal sealed class EntityToBuild : IEntityToBuild
public string Name { get; }
public string FullName { get; }
public string FullNameWithConstraints { get; }
public Constructor ConstructorToBuild { get; }
public Constructor? ConstructorToBuild { get; }
public IReadOnlyList<TypedSymbol> SettableProperties { get; }
public IReadOnlyList<TypedSymbol> ReadOnlyProperties { get; }
public string[] AdditionalNamespaces { get; }
public IEnumerable<BuildenatorDiagnostic> Diagnostics => _diagnostics;

public EntityToBuild(
INamedTypeSymbol typeForBuilder,
Expand All @@ -40,7 +40,7 @@ public EntityToBuild(
entityToBuildSymbol = typeForBuilder;
}

AdditionalNamespaces = additionalNamespaces.Concat(new[] { entityToBuildSymbol.ContainingNamespace.ToDisplayString() }).ToArray();
AdditionalNamespaces = additionalNamespaces.Concat([entityToBuildSymbol.ContainingNamespace.ToDisplayString()]).ToArray();
Name = entityToBuildSymbol.Name;
FullName = entityToBuildSymbol.ToDisplayString(new SymbolDisplayFormat(genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters, typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces));
FullNameWithConstraints = entityToBuildSymbol.ToDisplayString(new SymbolDisplayFormat(
Expand All @@ -52,13 +52,23 @@ public EntityToBuild(

public IReadOnlyList<ITypedSymbol> GetAllUniqueSettablePropertiesAndParameters()
{
if (ConstructorToBuild is null)
{
return _uniqueTypedSymbols ??= SettableProperties;
}

return _uniqueTypedSymbols ??= SettableProperties
.Where(x => !ConstructorToBuild.ContainsParameter(x.SymbolName))
.Concat(ConstructorToBuild.Parameters).ToList();
}

public IReadOnlyList<ITypedSymbol> GetAllUniqueReadOnlyPropertiesWithoutConstructorsParametersMatch()
{
if (ConstructorToBuild is null)
{
return _uniqueReadOnlyTypedSymbols ??= ReadOnlyProperties;
}

return _uniqueReadOnlyTypedSymbols ??= ReadOnlyProperties
.Where(x => !ConstructorToBuild.ContainsParameter(x.SymbolName)).ToList();
}
Expand All @@ -75,12 +85,11 @@ private static (TypedSymbol[] Settable, TypedSymbol[] ReadOnly) DivideProperties

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

internal sealed class Constructor
{
public bool IsPrivate { get; }

public static Constructor CreateConstructorOrDefault(
public static Constructor? CreateConstructorOrDefault(
INamedTypeSymbol entityToBuildSymbol,
IMockingProperties? mockingConfiguration,
IFixtureProperties? fixtureConfiguration,
Expand All @@ -91,25 +100,23 @@ public static Constructor CreateConstructorOrDefault(
.Where(m => m.DeclaredAccessibility == Accessibility.Public || m.DeclaredAccessibility == Accessibility.Internal)
.ToList();

var isPrivate = onlyPublicConstructors.Count == 0;
if (onlyPublicConstructors.Count == 0)
return default;

Dictionary<string, TypedSymbol> parameters = [];
if (!isPrivate)
{
parameters = onlyPublicConstructors
.OrderByDescending(x => x.Parameters.Length)
.First()
.Parameters
.ToDictionary(x => x.PascalCaseName(), s => new TypedSymbol(s, mockingConfiguration, fixtureConfiguration, nullableStrategy));
}

return new Constructor(parameters, isPrivate);

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

return new Constructor(parameters);
}

private Constructor(IReadOnlyDictionary<string, TypedSymbol> constructorParameters, bool isPrivate)
private Constructor(IReadOnlyDictionary<string, TypedSymbol> constructorParameters)
{
ConstructorParameters = constructorParameters;
IsPrivate = isPrivate;
}

public IReadOnlyDictionary<string, TypedSymbol> ConstructorParameters { get; }
Expand Down
24 changes: 24 additions & 0 deletions Buildenator/Diagnostics/BuildenatorDiagnostic.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Microsoft.CodeAnalysis;

namespace Buildenator.Diagnostics;

public sealed class BuildenatorDiagnostic
{
public BuildenatorDiagnostic(DiagnosticDescriptor descriptor, Location location, params object?[]? messageArgs)
{
Descriptor = descriptor;
Location = location;
MessageArgs = messageArgs;
}

public DiagnosticDescriptor Descriptor { get; }
public Location Location { get; }
public object?[]? MessageArgs { get; }

public static implicit operator Diagnostic(BuildenatorDiagnostic buildenatorDiagnostic)
=> Diagnostic.Create(
buildenatorDiagnostic.Descriptor,
buildenatorDiagnostic.Location,
buildenatorDiagnostic.MessageArgs);

}
47 changes: 47 additions & 0 deletions Buildenator/Diagnostics/BuildenatorDiagnosticDescriptors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Buildenator.Configuration;
using Microsoft.CodeAnalysis;

namespace Buildenator.Diagnostics;

internal static class BuildenatorDiagnosticDescriptors
{
internal static readonly DiagnosticDescriptor AbstractDiagnostic = new(
"BDN001",
"Cannot generate a builder for an abstract class",
"Cannot generate a builder for the {0} abstract class",
"Buildenator",
DiagnosticSeverity.Error,
true);

internal static readonly DiagnosticDescriptor NoPublicConstructorsDiagnostic = new(
"BDN002",
"Build method in the builder is missing for a class with no public constructor",
"Cannot generate a \"public {0} " + DefaultConstants.BuildMethodName + "() {/* content */}\" method for the {0} class that does not have public constructor. You have to add it by yourself.",
"Buildenator",
DiagnosticSeverity.Error,
true);

internal static readonly DiagnosticDescriptor BuildMethodOverridenDiagnostic = new(
"BDN003",
"You overriden the default " + DefaultConstants.BuildMethodName + "() method",
"You overriden the default " + DefaultConstants.BuildMethodName + "() method. if it's not on purpose, please remove it.",
"Buildenator",
DiagnosticSeverity.Info,
true);

internal static readonly DiagnosticDescriptor DefaultConstructorOverridenDiagnostic = new(
"BDN004",
"You overriden the default constructor method",
"You overriden the default constructor method. If it's not on purpose, please remove it.",
"Buildenator",
DiagnosticSeverity.Info,
true);

internal static readonly DiagnosticDescriptor DefaultParametersSetupOverridenDiagnostic = new(
"BDN005",
"You overriden the default method for setting up properties",
"You overriden the default method {0} for setting up properties. If it's not on purpose, please remove it.",
"Buildenator",
DiagnosticSeverity.Info,
true);
}
Loading

0 comments on commit bba5a3e

Please sign in to comment.