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

Tostring hoisting #688

Merged
merged 21 commits into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
2 changes: 2 additions & 0 deletions Vogen.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<s:String x:Key="/Default/CodeInspection/GeneratedCode/GeneratedFileMasks/=_002A_002Eg_002Ecs/@EntryIndexedValue">*.g.cs</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=BitwiseOperatorOnEnumWithoutFlags/@EntryIndexedValue">ERROR</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CheckNamespace/@EntryIndexedValue">ERROR</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=CognitiveComplexity/@EntryIndexedValue">SUGGESTION</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ConvertToPrimaryConstructor/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=InvalidXmlDocComment/@EntryIndexedValue">ERROR</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBeMadeStatic_002ELocal/@EntryIndexedValue">ERROR</s:String>
Expand Down Expand Up @@ -57,6 +58,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=Diags/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Errored/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Flintstone/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=formattable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Grohl/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=inheritdoc/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=initialisation/@EntryIndexedValue">True</s:Boolean>
Expand Down
12 changes: 6 additions & 6 deletions docs/nuget-readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

# What is the package?

This is a _semi_-opinionated library which is a Source Generator to generate [Value Objects](https://wiki.c2.com/?ValueObject).
The main goal is that the Value Objects generated have almost the same speed and memory performance as using primitives.
This is a _semi_-opinionated library which is a Source Generator to generate [value objects](https://wiki.c2.com/?ValueObject).
The main goal is that the value objects generated have almost the same speed and memory performance as using primitives.

The Value Objects wrap simple primitives such as `int`, `string`, `double` etc.
The value objects wrap simple primitives such as `int`, `string`, `double` etc.

To get started, add this package, and add a type such as:

Expand Down Expand Up @@ -34,7 +34,7 @@ void HandleCustomer(CustomerId customerId)

The Source Generator generates code for things like creating the object and for performing equality.

Value Objects help combat Primitive Obsession. Primitive Obsession means being obsessed with primitives. It is a Code Smell that degrades the quality of software.
Value objects help combat Primitive Obsession. Primitive Obsession means being obsessed with primitives. It is a Code Smell that degrades the quality of software.

> "*Primitive Obsession is using primitive data types to represent domain ideas*" [#](https://wiki.c2.com/?PrimitiveObsession)

Expand All @@ -45,7 +45,7 @@ Some examples:

The opinions are expressed as:

* A Value Object (VO) is constructed via a factory method named `From`, e.g. `Age.From(12)`
* A value object (VO) is constructed via a factory method named `From`, e.g. `Age.From(12)`
* A VO is equatable (`Age.From(12) == Age.From(12)`)
* A VO, if validated, is validated with a private static method named `Validate` that returns a `Validation` result
* Any validation that is not `Validation.Ok` results in a `ValueObjectValidationException` being thrown
Expand All @@ -70,7 +70,7 @@ public partial struct CustomerId
{
}
```
That's all you need to do to switch from a primitive to a Value Object.
That's all you need to do to switch from a primitive to a value object.

Here it is again with some validation

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
var codeAction = CodeAction.Create(
title: title,
createChangedDocument: c => GenerateValidationMethodAsync(context.Document, context.Diagnostics, declaration, c),
// createChangedSolution: c => GenerateValidationMethodAsync(context.Document, declaration, c),
equivalenceKey: title);

context.RegisterCodeFix(codeAction, diagnostic);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
var codeAction = CodeAction.Create(
title: title,
createChangedDocument: c => MakeMethodStatic(context.Document, declaration, c),
// createChangedSolution: c => GenerateValidationMethodAsync(context.Document, declaration, c),
equivalenceKey: title);

context.RegisterCodeFix(codeAction, diagnostic);
Expand Down
42 changes: 34 additions & 8 deletions src/Vogen/BuildWorkItems.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ internal static class BuildWorkItems
UserProvidedOverloads userProvidedOverloads =
DiscoverUserProvidedOverloads.Discover(voSymbolInformation, underlyingType);

ThrowIfToStringOverrideOnRecordIsUnsealed(target, context, userProvidedOverloads.ToStringInfo);
ThrowIfAnyToStringOverrideOnRecordIsUnsealed(target, context, userProvidedOverloads.ToStringOverloads);

MethodDeclarationSyntax? validateMethod = null;
MethodDeclarationSyntax? normalizeInputMethod = null;
Expand Down Expand Up @@ -139,6 +139,8 @@ RecordDeclarationSyntax rds when rds.IsKind(SyntaxKind.RecordStructDeclaration)
IsTheWrapperAValueType = isWrapperAValueType,

ParsingInformation = BuildParsingInformation(underlyingType, vogenKnownSymbols),
//FormattableInformation = BuildFormattableInformation(underlyingType, vogenKnownSymbols),
ToStringInformation = BuildToStringInformation(underlyingType),
Config = config,

UserProvidedOverloads = userProvidedOverloads,
Expand Down Expand Up @@ -186,22 +188,46 @@ private static ParsingInformation BuildParsingInformation(INamedTypeSymbol under
return parsingInformation;
}


private static ToStringInformation BuildToStringInformation(INamedTypeSymbol underlyingType)
{
ToStringInformation info = new()
{
ToStringMethodsOnThePrimitive = MethodDiscovery.FindToStringMethodsOnThePrimitive(underlyingType).ToList(),

UnderlyingTypeHasADefaultToStringMethod = underlyingType.GetMembers().OfType<IMethodSymbol>().Any(m => m is { Name: "ToString", Parameters.Length: 0 })
};

return info;
}

private static bool DoesPubliclyImplementGenericInterface(INamedTypeSymbol underlyingType, INamedTypeSymbol? openGeneric)
{
INamedTypeSymbol? closedGeneric = openGeneric?.Construct(underlyingType);
return MethodDiscovery.DoesPrimitivePubliclyImplementThisInterface(underlyingType, closedGeneric);
}

private static void ThrowIfToStringOverrideOnRecordIsUnsealed(VoTarget target,
private static void ThrowIfAnyToStringOverrideOnRecordIsUnsealed(VoTarget target,
SourceProductionContext context,
UserProvidedToString info)
UserProvidedToStringMethods infos)
{
if (info is { WasSupplied: true, Method: not null, IsRecordClass: true, IsSealed: false })
foreach (IMethodSymbol info in infos)
{
context.ReportDiagnostic(
DiagnosticsCatalogue.RecordToStringOverloadShouldBeSealed(
info.Method.Locations[0],
target.VoSymbolInformation.Name));
// only report on implementations withing the value object itself, and not any derived methods.
var voSymbol = target.VoSymbolInformation;

if (!SymbolEqualityComparer.Default.Equals(info.ContainingType, voSymbol))
{
continue;
}

if(voSymbol.IsRecordClass() && !info.IsSealed)
{
context.ReportDiagnostic(
DiagnosticsCatalogue.RecordToStringOverloadShouldBeSealed(
info.Locations[0],
voSymbol.Name));
}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/Vogen/CompilationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.CodeAnalysis.CSharp;
// ReSharper disable All

namespace Vogen;

Expand Down
2 changes: 1 addition & 1 deletion src/Vogen/Diagnostics/DiagnosticsCatalogue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ internal static class DiagnosticsCatalogue
private static readonly DiagnosticDescriptor _recordToStringOverloadShouldBeSealed = CreateDescriptor(
RuleIdentifiers.RecordToStringOverloadShouldBeSealed,
"Overrides of ToString on records should be sealed to differentiate it from the C# compiler-generated method. See https://github.com/SteveDunn/Vogen/wiki/Records#tostring for more information.",
"ToString overrides should be sealed on records. See https://github.com/SteveDunn/Vogen/wiki/Records#tostring for more information.");
"ToString overrides should be sealed on records. See https://stevedunn.github.io/Vogen/records.html#tostring for more information.");

private static readonly DiagnosticDescriptor _typeShouldBePartial = CreateDescriptor(
RuleIdentifiers.TypeShouldBePartial,
Expand Down
135 changes: 8 additions & 127 deletions src/Vogen/DiscoverUserProvidedOverloads.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public static UserProvidedOverloads Discover(INamedTypeSymbol vo, INamedTypeSymb

return new UserProvidedOverloads
{
ToStringInfo = HasToStringOverload(vo),
ToStringOverloads = new UserProvidedToStringMethods(
MethodDiscovery.GetAnyUserProvidedToStringMethods(vo).ToList()),

HashCodeInfo = HasGetHashCodeOverload(vo),

Expand All @@ -25,7 +26,8 @@ public static UserProvidedOverloads Discover(INamedTypeSymbol vo, INamedTypeSymb
EqualsForUnderlying = new UserProvidedEqualsForUnderlying(WasProvided: equalsForUnderlying is not null) ,

ParseMethods = DiscoverParseMethods(vo, underlyingType),
TryParseMethods = DiscoverTryParseMethods(vo)
TryParseMethods = DiscoverTryParseMethods(vo),
TryFormatMethods = DiscoverTryFormatMethods(vo)
};
}

Expand All @@ -43,136 +45,14 @@ private static UserProvidedTryParseMethods DiscoverTryParseMethods(INamedTypeSym
MethodDiscovery.TryGetUserSuppliedTryParseMethods(wrapperType).ToList());
}

private static UserProvidedToString HasToStringOverload(ITypeSymbol typeSymbol)
private static UserProvidedTryFormatMethods DiscoverTryFormatMethods(INamedTypeSymbol wrapperType)
{
IMethodSymbol? method = MethodDiscovery.TryGetToStringOverride(typeSymbol);
return method is null
? UserProvidedToString.NotProvided
: new UserProvidedToString(
WasSupplied: true,
IsRecordClass: typeSymbol is { IsRecord: true, IsReferenceType: true },
IsSealed: method.IsSealed,
method);
return new UserProvidedTryFormatMethods(
MethodDiscovery.TryGetUserSuppliedTryFormatMethods(wrapperType).ToList());
}

private static UserProvidedGetHashCode HasGetHashCodeOverload(ITypeSymbol typeSymbol) =>
new(WasProvided: MethodDiscovery.TryGetHashCodeOverload(typeSymbol) is not null);

// private static UserProvidedEqualsForWrapper HasUserGeneratedEqualsForWrapper(
// ITypeSymbol vo,
// INamedTypeSymbol? wrapperType)
// {
// while (true)
// {
// var matchingMethods = vo.GetMembers("Equals").OfType<IMethodSymbol>();
//
// foreach (IMethodSymbol eachMethod in matchingMethods)
// {
// if (eachMethod.IsImplicitlyDeclared)
// {
// continue;
// }
//
// // can't change access rights
// if (IsNotPublicOrProtected(eachMethod))
// {
// continue;
// }
//
// if (DoesNotHaveJustOneParameter(eachMethod))
// {
// continue;
// }
//
// IParameterSymbol onlyParameter = eachMethod.Parameters[0];
//
// if (SymbolEqualityComparer.Default.Equals(onlyParameter, wrapperType))
// {
// continue;
// }
//
// return new UserProvidedEqualsForWrapper(
// WasProvided: true);
// }
//
// INamedTypeSymbol? baseType = vo.BaseType;
//
// if (baseType is null)
// {
// return new UserProvidedEqualsForWrapper(WasProvided: false);
// }
//
// if (CannotGoFurtherInHierarchy(baseType))
// {
// return new UserProvidedEqualsForWrapper(WasProvided: false);
// }
//
// vo = baseType;
// }
// }

// private static UserProvidedEqualsForUnderlying HasUserGeneratedEqualsForUnderlying(
// INamedTypeSymbol vo,
// ITypeSymbol primitiveType)
// {
// while (true)
// {
// var matchingMethods = vo.GetMembers("Equals").OfType<IMethodSymbol>();
//
// foreach (IMethodSymbol eachMethod in matchingMethods)
// {
// if (eachMethod.IsImplicitlyDeclared)
// {
// continue;
// }
//
// // can't change access rights
// if (IsNotPublicOrProtected(eachMethod))
// {
// continue;
// }
//
// if (DoesNotHaveJustOneParameter(eachMethod))
// {
// continue;
// }
//
// IParameterSymbol onlyParameter = eachMethod.Parameters[0];
//
// if (SymbolEqualityComparer.Default.Equals(onlyParameter.Type, primitiveType))
// {
// return new UserProvidedEqualsForUnderlying(WasProvided: true);
// }
// }
//
// INamedTypeSymbol? baseType = primitiveType.BaseType;
//
// if (baseType is null)
// {
// return new UserProvidedEqualsForUnderlying(WasProvided: false);
// }
//
// if (CannotGoFurtherInHierarchy(baseType))
// {
// return new UserProvidedEqualsForUnderlying(WasProvided: false);
// }
//
// primitiveType = baseType;
// }
// }

// private static bool CannotGoFurtherInHierarchy(INamedTypeSymbol baseType) =>
// baseType.SpecialType is SpecialType.System_Object or SpecialType.System_ValueType;
//
// private static bool DoesNotHaveJustOneParameter(IMethodSymbol eachMethod) => eachMethod.Parameters.Length != 1;
//
// private static bool IsNotPublicOrProtected(IMethodSymbol eachMethod) =>
// eachMethod.DeclaredAccessibility is not (Accessibility.Public or Accessibility.Protected);
}

public record struct UserProvidedToString(bool WasSupplied, bool IsRecordClass, bool IsSealed, IMethodSymbol? Method)
{
public static readonly UserProvidedToString NotProvided = new(false, false, false, null);
}

public record struct UserProvidedGetHashCode(bool WasProvided);
Expand Down Expand Up @@ -233,6 +113,7 @@ static bool HasSameParameters(IMethodSymbol usersMethod, IMethodSymbol methodFro
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}


/// <summary>
/// Represents the TryParse methods that the user supplied.
/// Every item is guaranteed to be static, named 'TryParse', returns a bool,
Expand Down
2 changes: 1 addition & 1 deletion src/Vogen/EfCoreConverterSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ namespace Vogen;
/// <param name="VoSymbol">The symbol for the value object being referenced. In effect, the generic value of the attribute.</param>
/// <param name="UnderlyingType">The symbol for the underlying type that is represented by the value object.</param>
/// <param name="SourceType">The symbol for the method that contains the `EfCoreConverter` attribute(s).</param>
internal record class EfCoreConverterSpec(INamedTypeSymbol VoSymbol, INamedTypeSymbol UnderlyingType, INamedTypeSymbol SourceType);
internal record EfCoreConverterSpec(INamedTypeSymbol VoSymbol, INamedTypeSymbol UnderlyingType, INamedTypeSymbol SourceType);
14 changes: 1 addition & 13 deletions src/Vogen/Extensions/IEnumerableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,7 @@ public static IEnumerable<T> Order<T>(this IEnumerable<T> source) where T : ICom
{
return source.OrderBy((t1, t2) => t1.CompareTo(t2));
}

private static readonly Func<object?, bool> s_notNullTest = x => x != null;

public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source) where T : class
{
if (source == null)
{
return ImmutableArray<T>.Empty;
}

return source.Where((Func<T?, bool>)s_notNullTest)!;
}


public static ImmutableArray<TSource> WhereAsArray<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> selector)
{
var builder = ImmutableArray.CreateBuilder<TSource>();
Expand Down
Loading