Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate IEquatable.Equals, object.Equals, object.GetHashCode implementations #56

Merged
merged 33 commits into from
Aug 5, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
93cf1ea
First implementation of Equality Comparisons, work in progress
fl3pp Jul 23, 2019
f3bdc9a
Adjusted Equality Comparison
fl3pp Jul 23, 2019
7465184
Updated csproj to not contain changes
fl3pp Jul 23, 2019
34cf6b9
Implemented attribute arguments
fl3pp Jul 24, 2019
06dc363
Refactorings:
fl3pp Jul 24, 2019
f4ec7cb
small refactorings
fl3pp Jul 24, 2019
7cba987
Repairing build
fl3pp Jul 24, 2019
8df573f
Removed Helper classes
fl3pp Jul 25, 2019
3ffd965
Removed and Sorted usings
fl3pp Jul 25, 2019
2edb935
Created shared Descriptors File
fl3pp Jul 25, 2019
590cef3
fixed build
fl3pp Jul 25, 2019
bed9788
- Moved diagnostics back to Analyzers project, linked from Generators…
fl3pp Jul 25, 2019
ca8eff6
Implemented usage of operator in equals when comparing well-known types
fl3pp Jul 26, 2019
808aed9
Added Support for nullable well-known types
fl3pp Jul 26, 2019
5978ef6
Repairing build
fl3pp Jul 26, 2019
ffdea03
Added Semantic Information to descriptor
fl3pp Jul 26, 2019
a7a6759
Fixed EqualityUsageAnalyzer, see comment 56#discussion_r307672169
fl3pp Jul 26, 2019
801cbd2
Merging from upstream master in fork master
fl3pp Jul 29, 2019
d8cc498
Merging from master to fork
fl3pp Jul 30, 2019
cb5849f
Added feature documentation
fl3pp Jul 31, 2019
613ce00
Using ThisAssembly instead of reflection in GeneratedCodeAttributeGen…
fl3pp Jul 31, 2019
6cc1bf7
Refactored ObjectEqualsGenerator and EquatableEqualsGenerator into ow…
fl3pp Jul 31, 2019
2938bb4
- Adjusted Feature comments
fl3pp Jul 31, 2019
79268a9
Support standalone Equals implementation
fl3pp Aug 2, 2019
efcb589
Refactorings according to PR 56
fl3pp Aug 2, 2019
48c5a9a
Implemented operator equality code generation
fl3pp Aug 2, 2019
6fe61f1
Adjusted EqualityUsageAnalyzer syntax
fl3pp Aug 2, 2019
bdc0180
Fix bit grouping style for Features
amis92 Aug 5, 2019
0ce3eeb
Reformatted docs on equality Features
amis92 Aug 5, 2019
240e4c6
Refactor methods into project style
amis92 Aug 5, 2019
052d6b5
Prevent equality generation for non-sealed classes
amis92 Aug 5, 2019
43a1813
stupid typo
amis92 Aug 5, 2019
685cb5b
Add changelog entries
amis92 Aug 5, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/Amadevus.RecordGenerator.Attributes/RecordAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,17 @@
[Conditional("CodeGeneration")]
public sealed class RecordAttribute : Attribute
{
public bool SkipEquality { get; }
public bool SkipBuilder { get; }
public bool SkipDeconstruct { get; }

public RecordAttribute(
bool skipEquality = false,
bool skipBuilder = false,
bool skipDeconstruct = false)
{
SkipEquality = skipEquality;
SkipBuilder = skipBuilder;
SkipDeconstruct = skipDeconstruct;
}
}
301 changes: 301 additions & 0 deletions src/Amadevus.RecordGenerator.Generators/EqualityPartialGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;

namespace Amadevus.RecordGenerator.Generators
{
internal sealed class EqualityPartialGenerator : PartialGeneratorBase
{
private readonly RecordDescriptor descriptor;
private readonly EqualityPartialCodeGenerator generator;

private NameSyntax ClassIdentifier
{
get => SyntaxExtensions.GetTypeSyntax(descriptor.TypeDeclaration);
}

private IEnumerable<RecordDescriptor.Entry> Entries => descriptor.Entries;

public EqualityPartialGenerator(
RecordDescriptor descriptor,
CancellationToken cancellationToken)
: base(descriptor, cancellationToken)
{
this.descriptor = descriptor;
this.generator = new EqualityPartialCodeGenerator();
}

public static TypeDeclarationSyntax Generate(RecordDescriptor descriptor, CancellationToken cancellationToken)
{
var generator = new EqualityPartialGenerator(descriptor, cancellationToken);
return generator.GenerateTypeDeclaration();
}

protected override BaseListSyntax GenerateBaseList()
{
return generator.GenerateBaseListSyntax(ClassIdentifier);
}

protected override SyntaxList<MemberDeclarationSyntax> GenerateMembers()
{
return List(new MemberDeclarationSyntax[]
{
generator.GenerateGenericEquals(ClassIdentifier),
generator.GenerateObjectEquals(ClassIdentifier, Entries),
generator.GenerateGetHashCode(Entries),
});
}
}

internal sealed class EqualityPartialCodeGenerator
{
private const string equalsMethodName = "Equals";
private const string varIdentifierName = "var";

private const string systemNamespaceName = "System";
private const string collectionsNamespaceName = "Collections";
private const string genericNamespaceName = "Generic";
private const string equalityComparerName = "EqualityComparer";
private const string equalityComparerDefaultProperty = "Default";

public BaseListSyntax GenerateBaseListSyntax(NameSyntax identifier)
{
const string equotableInterfaceName = "IEquatable";

return BaseList(
SingletonSeparatedList<BaseTypeSyntax>(
SimpleBaseType(
QualifiedName(
IdentifierName(systemNamespaceName),
GenericName(
Identifier(equotableInterfaceName))
.WithTypeArgumentList(
TypeArgumentList(
SingletonSeparatedList<TypeSyntax>(
identifier)))))));
}

public MemberDeclarationSyntax GenerateGenericEquals(NameSyntax identifier)
{
const string objVariableName = "obj";

var method = MethodDeclaration(
PredefinedType(
Token(SyntaxKind.BoolKeyword)),
Identifier(equalsMethodName));

var modifiers = TokenList(
new[] {
Token(SyntaxKind.PublicKeyword),
Token(SyntaxKind.OverrideKeyword)});

var parameterList = ParameterList(
SingletonSeparatedList(
Parameter(
Identifier(objVariableName))
.WithType(
PredefinedType(
Token(SyntaxKind.ObjectKeyword)))));

var equotableEqualsInvocation = InvocationExpression(
IdentifierName(equalsMethodName))
.WithArgumentList(
ArgumentList(
SingletonSeparatedList(
Argument(
BinaryExpression(
SyntaxKind.AsExpression,
IdentifierName(objVariableName),
identifier)))));

var body = Block(
SingletonList<StatementSyntax>(
ReturnStatement(equotableEqualsInvocation)));

return method
.WithModifiers(modifiers)
.WithParameterList(parameterList)
.WithBody(body);
}

public MemberDeclarationSyntax GenerateObjectEquals(
NameSyntax identifierName,
IEnumerable<RecordDescriptor.Entry> propertiesToCompare)
{
const string otherVariableName = "other";

var method = MethodDeclaration(
PredefinedType(
Token(SyntaxKind.BoolKeyword)),
Identifier(equalsMethodName));

var modifiers = TokenList(Token(SyntaxKind.PublicKeyword));

var parameterList = ParameterList(
SingletonSeparatedList(
Parameter(
Identifier(otherVariableName))
.WithType(identifierName)));

var equalsExpressions = propertiesToCompare.Select(property =>
{
return InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName(systemNamespaceName),
IdentifierName(collectionsNamespaceName)),
IdentifierName(genericNamespaceName)),
GenericName(
Identifier(equalityComparerName))
.WithTypeArgumentList(
TypeArgumentList(
SingletonSeparatedList(
property.Type)))),
IdentifierName(equalityComparerDefaultProperty)),
IdentifierName(equalsMethodName)))
.WithArgumentList(
ArgumentList(
SeparatedList<ArgumentSyntax>(
new SyntaxNodeOrToken[]{
Argument(
IdentifierName(property.Identifier.Text)),
Token(SyntaxKind.CommaToken),
Argument(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName(otherVariableName),
IdentifierName(property.Identifier.Text)))})));
}).ToList();

var chainedAndExpression = BinaryExpression(
SyntaxKind.NotEqualsExpression,
IdentifierName("other"),
LiteralExpression(
SyntaxKind.NullLiteralExpression));

foreach (var equalsExpression in equalsExpressions)
{
chainedAndExpression = BinaryExpression(
SyntaxKind.LogicalAndExpression,
chainedAndExpression,
equalsExpression);
}

var body = Block(
SingletonList<StatementSyntax>(
ReturnStatement(chainedAndExpression)));

return method
.WithModifiers(modifiers)
.WithParameterList(parameterList)
.WithBody(body);
}

public MemberDeclarationSyntax GenerateGetHashCode(
IEnumerable<RecordDescriptor.Entry> propertiesToCompare)
{
const string getHashCodeMethodName = "GetHashCode";
const string hashCodeVariableName = "hashCode";
const int hashCodeInitialValue = 2085527896;
const int hashCodeMultiplicationValue = 1521134295;

var method = MethodDeclaration(
PredefinedType(
Token(SyntaxKind.IntKeyword)),
Identifier(getHashCodeMethodName));

var modifiers = TokenList(
new []{
Token(SyntaxKind.PublicKeyword),
Token(SyntaxKind.OverrideKeyword)});

var hashCodeVariableDeclaration = LocalDeclarationStatement(
VariableDeclaration(
IdentifierName(varIdentifierName))
.WithVariables(
SingletonSeparatedList(
VariableDeclarator(
Identifier(hashCodeVariableName))
.WithInitializer(
EqualsValueClause(
LiteralExpression(
SyntaxKind.NumericLiteralExpression,
Literal(hashCodeInitialValue)))))));

var hashCodeAssignments = propertiesToCompare.Select(property =>
{
return ExpressionStatement(
AssignmentExpression(
SyntaxKind.SimpleAssignmentExpression,
IdentifierName(hashCodeVariableName),
BinaryExpression(
SyntaxKind.AddExpression,
BinaryExpression(
SyntaxKind.MultiplyExpression,
IdentifierName(hashCodeVariableName),
PrefixUnaryExpression(
SyntaxKind.UnaryMinusExpression,
LiteralExpression(
SyntaxKind.NumericLiteralExpression,
Literal(hashCodeMultiplicationValue)))),
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName(systemNamespaceName),
IdentifierName(collectionsNamespaceName)),
IdentifierName(genericNamespaceName)),
GenericName(
Identifier(equalityComparerName))
.WithTypeArgumentList(
TypeArgumentList(
SingletonSeparatedList(property.Type)))),
IdentifierName(equalityComparerDefaultProperty)),
IdentifierName(getHashCodeMethodName))).WithArgumentList(
ArgumentList(
SingletonSeparatedList(
Argument(
IdentifierName(property.Identifier.Text))))))));
}).Cast<StatementSyntax>();

var returnStatement = ReturnStatement(
IdentifierName(hashCodeVariableName));

var innerBody = hashCodeAssignments.ToList();
innerBody.Insert(0, hashCodeVariableDeclaration);
innerBody.Add(returnStatement);

var body = Block(
SingletonList<StatementSyntax>(
CheckedStatement(
SyntaxKind.UncheckedStatement,
Block(innerBody))));

return method
.WithModifiers(modifiers)
.WithBody(body);
}
}
}
5 changes: 4 additions & 1 deletion src/Amadevus.RecordGenerator.Generators/RecordGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ namespace Amadevus.RecordGenerator.Generators
{
public class RecordGenerator : ICodeGenerator
{
private readonly RecordGeneratorOptions options;

public RecordGenerator(AttributeData attributeData)
{

options = RecordGeneratorOptions.FromAttributeData(attributeData);
}

public Task<SyntaxList<MemberDeclarationSyntax>> GenerateAsync(TransformationContext context, IProgress<Diagnostic> progress, CancellationToken cancellationToken)
Expand All @@ -35,6 +37,7 @@ IEnumerable<MemberDeclarationSyntax> GenerateRecordPartials(RecordDescriptor des
yield return RecordPartialGenerator.Generate(descriptor, cancellationToken);
yield return BuilderPartialGenerator.Generate(descriptor, cancellationToken);
yield return DeconstructPartialGenerator.Generate(descriptor, cancellationToken);
yield return EqualityPartialGenerator.Generate(descriptor, cancellationToken);
yield break;
}
}
Expand Down
52 changes: 52 additions & 0 deletions src/Amadevus.RecordGenerator.Generators/RecordGeneratorOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
using Microsoft.CodeAnalysis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace Amadevus.RecordGenerator.Generators
{
public class RecordGeneratorOptions
{
public bool SkipEquality { get; }
public bool SkipBuilder { get; }
public bool SkipDeconstruct { get; }

public RecordGeneratorOptions(
bool skipEquality, bool skipBuilder, bool skipDeconstruct)
{
SkipEquality = skipEquality;
SkipBuilder = skipBuilder;
SkipDeconstruct = skipDeconstruct;
}

public static RecordGeneratorOptions FromAttributeData(
AttributeData attributeData)
{
return new RecordGeneratorOptionsReader().ReadOptions(attributeData);
}
}

public class RecordGeneratorOptionsReader
{
public RecordGeneratorOptions ReadOptions(AttributeData attributeData)
{
var attributeArguments = attributeData.AttributeClass.Constructors.Single()
.Parameters.Select((parameter, index) => new
{
parameter.Name,
attributeData.ConstructorArguments[index].Value
});

var optionsConstructor = typeof(RecordGeneratorOptions).GetConstructors().Single();
var optionsConstructorParameters = optionsConstructor.GetParameters();

return (RecordGeneratorOptions)optionsConstructor.Invoke(
amis92 marked this conversation as resolved.
Show resolved Hide resolved
optionsConstructorParameters
.Select(p => attributeArguments.Single(a => a.Name.Equals(p.Name)).Value)
.ToArray());
}
}
}