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

feat: support for implicit conversion from raw to refined #19

Merged
merged 4 commits into from
Jan 8, 2025
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,10 @@ private static void Analyze(SyntaxNodeAnalysisContext ctx)
var hasStringConstructor = ctx
.Node.DescendantNodes()
.OfType<AttributeArgumentSyntax>()
.Any(arg => arg.Expression is LiteralExpressionSyntax);
.Any(arg =>
arg.Expression is LiteralExpressionSyntax literal
&& literal.IsKind(SyntaxKind.StringLiteralExpression)
);

var hasStringReturn = ctx
.Node.Ancestors()
Expand Down
5 changes: 5 additions & 0 deletions Tuxedo.SourceGenerator/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ public static string StripExpressionParts(this string value)
return value == null ? null : render(value);
}

public static string? RenderIfTrue(this bool value, Func<string> render)
{
return !value ? null : render();
}

public static string? LowercaseFirst(this string? value)
{
return value == null ? null : char.ToLowerInvariant(value[0]) + value.Substring(1);
Expand Down
7 changes: 7 additions & 0 deletions Tuxedo.SourceGenerator/Extensions/SymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,11 @@ public static bool IsRefinedType(this INamedTypeSymbol symbol)
is true
);
}

public static bool HasInterface(this ITypeSymbol? symbol, string interfaceName)
{
return symbol?.AllInterfaces.Any(i =>
i.ToDisplayString().Equals(interfaceName, StringComparison.Ordinal)
) ?? false;
}
bmazzarol marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ internal sealed class RefinementAttribute : Attribute
/// </summary>
public string? Name { get; set; }

/// <summary>
/// Indicates whether the raw type has an implicit conversion to the refined type.
/// The default is false, which generates an explicit conversion.
/// </summary>
public bool HasImplicitConversionFromRaw { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="RefinementAttribute"/> class
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ private static string RenderMultiRefinedType(RefinedTypeDetails model)

{{RenderEqualityMembers(model)}}

{{RenderFormattingMembers(model)}}

/// <summary>
/// Standard deconstruction to the underlying values
/// </summary>
/// <param name="value">raw {{model.RawType.EscapeXml()}}</param>
/// <param name="altValue">alternative {{model.AlternativeType.EscapeXml()}}</param>
/// <param name="altValue">The alternative {{model.AlternativeType.EscapeXml()}} produced when the refinement predicate is satisfied</param>
bmazzarol marked this conversation as resolved.
Show resolved Hide resolved
public void Deconstruct(out {{model.RawType}} value, out {{model.AlternativeType}} altValue)
{
value = Value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@ namespace Tuxedo.SourceGenerator;

public sealed partial class RefinementSourceGenerator
{
private readonly record struct RefinedTypeDetails(
private sealed record RefinedTypeDetails(
string? Namespace,
SyntaxList<UsingDirectiveSyntax> Usings,
string? Predicate,
bool PredicateReturnsFailureMessage,
string? FailureMessage,
string? AccessModifier,
RefinementAttributeParts AttributeDetails,
string? Generics,
string? GenericConstraints,
string? RawType,
ITypeSymbol? RawTypeSymbol,
string? RefinedType,
string? AlternativeType
string? AlternativeType,
ITypeSymbol? AlternativeTypeSymbol
)
{
public string? RefinedTypeXmlSafeName => (RefinedType + Generics).EscapeXml();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ namespace Tuxedo.SourceGenerator;

public sealed partial class RefinementSourceGenerator
{
private readonly ref struct RefinementAttributeParts
private sealed record RefinementAttributeParts
{
public string? FailureMessage { get; }
private bool IsInternal { get; }
public string AccessModifier => IsInternal ? "internal" : "public";
public string? Name { get; }
public bool HasImplicitConversionFromRaw { get; }

public RefinementAttributeParts(MethodDeclarationSyntax methodDeclaration)
{
Expand Down Expand Up @@ -39,12 +40,18 @@ public RefinementAttributeParts(MethodDeclarationSyntax methodDeclaration)
.Where(x => x.NameEquals is null)
.Select(x => x.Expression.ToString())
.FirstOrDefault();
IsInternal =
nameToArgs.TryGetValue(nameof(IsInternal), out var value)
&& string.Equals(value, "true", StringComparison.Ordinal);
Name = nameToArgs.TryGetValue(nameof(Name), out var nameToName)
? nameToName.StripExpressionParts()
: null;
IsInternal = IsEnabled(nameof(IsInternal), nameToArgs);
HasImplicitConversionFromRaw = IsEnabled(
nameof(HasImplicitConversionFromRaw),
nameToArgs
);
}

private static bool IsEnabled(string value, Dictionary<string?, string> nameToArgs) =>
nameToArgs.TryGetValue(value, out var v)
&& string.Equals(v, "true", StringComparison.Ordinal);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ private static string RenderSingleRefinedType(RefinedTypeDetails model)
{{RenderTryParseMethod(model)}}

{{RenderEqualityMembers(model)}}

{{RenderFormattingMembers(model)}}
}
""";
}
Expand All @@ -50,14 +52,15 @@ namespace {model.Namespace};

private static string RenderTypeNameParts(RefinedTypeDetails model)
{
var isFormattable = model.RawTypeSymbol.HasInterface("System.IFormattable");
return $"""
/// <summary>
/// A refined {model.RawType.EscapeXml()} based on the {model.Predicate.EscapeXml()} refinement predicate{model.AlternativeType.RenderIfNotNull(
x => $" which produces an alternative {x.EscapeXml()} value"
)}
/// </summary>
[RefinedType]
{model.AccessModifier} readonly partial struct {model.RefinedType}{model.Generics} : IEquatable<{model.RefinedType}{model.Generics}>{model.GenericConstraints.PrependIfNotNull(
{model.AttributeDetails.AccessModifier} readonly partial struct {model.RefinedType}{model.Generics} : IEquatable<{model.RefinedType}{model.Generics}>{isFormattable.RenderIfTrue(() => ", IFormattable")}{model.GenericConstraints.PrependIfNotNull(
"\n\t"
)}
""";
Expand Down Expand Up @@ -103,12 +106,16 @@ private static string RenderParseMethod(RefinedTypeDetails model)
{
return $$"""
/// <summary>
/// Explicit conversion from a {{model.RawType.EscapeXml()}} to a {{model.RefinedTypeXmlSafeName}}
/// {{(
model.AttributeDetails.HasImplicitConversionFromRaw ? "Implicit" : "Explicit"
)}} conversion from a {{model.RawType.EscapeXml()}} to a {{model.RefinedTypeXmlSafeName}}
/// </summary>
/// <param name="value">raw {{model.RawType.EscapeXml()}}</param>
/// <returns>refined {{model.RefinedTypeXmlSafeName}}</returns>
/// <exception cref="ArgumentOutOfRangeException">if the {{model.Predicate.EscapeXml()}} refinement fails</exception>
public static explicit operator {{model.RefinedType}}{{model.Generics}}({{model.RawType}} value)
public static {{(
model.AttributeDetails.HasImplicitConversionFromRaw ? "implicit" : "explicit"
)}} operator {{model.RefinedType}}{{model.Generics}}({{model.RawType}} value)
{
return Parse(value);
}
Expand Down Expand Up @@ -144,15 +151,15 @@ public static bool TryParse(
{
if ({{model.Predicate}}{{model.Generics}}(value{{model.AlternativeType.RenderIfNotNull(
_ => ", out var altValue"
)}}){{(model.PredicateReturnsFailureMessage ? " is not {} fm": "")}})
)}}){{model.PredicateReturnsFailureMessage.RenderIfTrue(() =>" is not {} fm")}})
{
refined = new {{model.RefinedType}}{{model.Generics}}(value{{model.AlternativeType.RenderIfNotNull(_ => ", altValue")}});
failureMessage = null;
return true;
}

refined = default!;
failureMessage = {{(model.PredicateReturnsFailureMessage ? "fm" : $"${model.FailureMessage}")}};
refined = default;
failureMessage = {{(model.PredicateReturnsFailureMessage ? "fm" : $"${model.AttributeDetails.FailureMessage}")}};
return false;
}
""";
Expand Down Expand Up @@ -199,4 +206,51 @@ public override int GetHashCode()
}
""";
}

private static string RenderFormattingMembers(RefinedTypeDetails details)
{
var isConvertible = details.RawTypeSymbol?.HasInterface("System.IConvertible") ?? false;
var isFormattable = details.RawTypeSymbol?.HasInterface("System.IFormattable") ?? false;
return $$"""
/// <summary>
/// Returns the string representation of the underlying {{details.RawType.EscapeXml()}}
/// </summary>
public override string ToString()
{
return Value.ToString() ?? string.Empty;
}{{isConvertible.RenderIfTrue(RenderConvertableImpl)}}{{isFormattable.RenderIfTrue(
RenderFormattableImpl
)}}
""";

string RenderConvertableImpl()
{
return $$"""


/// <summary>
/// Returns the string representation of the underlying {{details.RawType.EscapeXml()}}
/// </summary>
public string ToString(IFormatProvider? provider)
{
return ((IConvertible)Value).ToString(provider) ?? string.Empty;
}
""";
}

string RenderFormattableImpl()
{
return $$"""


/// <summary>
/// Returns the string representation of the underlying {{details.RawType.EscapeXml()}}
/// </summary>
public string ToString(string? format, IFormatProvider? formatProvider)
{
return ((IFormattable)Value).ToString(format, formatProvider) ?? string.Empty;
}
""";
}
}
}
11 changes: 6 additions & 5 deletions Tuxedo.SourceGenerator/Generators/RefinementSourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,6 @@ CancellationToken token

// get the attribute details
var attributeParts = new RefinementAttributeParts(methodDeclarationSyntax);
var failureMessage = attributeParts.FailureMessage;
var accessModifier = attributeParts.AccessModifier;

// extract the generic parts
ExtractGenericPartDetails(
Expand All @@ -119,27 +117,30 @@ attributeParts.Name is not null

// try and see if there is a second out parameter
string? altType = null;
ITypeSymbol? altTypeSymbol = null;
if (methodDeclarationSyntax.ParameterList.Parameters.Count > 1)
{
var secondParam = methodDeclarationSyntax.ParameterList.Parameters[1].Type!;
var secondParameterTypeInfo = ctx
.SemanticModel.GetTypeInfo(secondParam, cancellationToken: token)
.Type;
altType = secondParameterTypeInfo!.ToDisplayString();
altTypeSymbol = secondParameterTypeInfo;
}

return new RefinedTypeDetails(
Namespace: ns,
Usings: usings,
Predicate: predicate,
PredicateReturnsFailureMessage: returningFailureMessage,
FailureMessage: failureMessage,
AccessModifier: accessModifier,
AttributeDetails: attributeParts,
Generics: generics,
GenericConstraints: genericTypeConstraints,
RawType: rawType,
RawTypeSymbol: firstParameterTypeInfo,
RefinedType: refinedType,
AlternativeType: altType
AlternativeType: altType,
AlternativeTypeSymbol: altTypeSymbol
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public static bool TryParse(
return true;
}

refined = default!;
refined = default;
failureMessage = $"The boolean value must be 'False', instead found '{value}'";
return false;
}
Expand Down Expand Up @@ -112,4 +112,20 @@ public override int GetHashCode()
{
return HashCode.Combine(_value);
}

/// <summary>
/// Returns the string representation of the underlying bool
/// </summary>
public override string ToString()
{
return Value.ToString() ?? string.Empty;
}

/// <summary>
/// Returns the string representation of the underlying bool
/// </summary>
public string ToString(IFormatProvider? provider)
{
return ((IConvertible)Value).ToString(provider) ?? string.Empty;
}
bmazzarol marked this conversation as resolved.
Show resolved Hide resolved
}
18 changes: 17 additions & 1 deletion Tuxedo.Tests/BoolRefinementsTests.Case5#TrueBool.g.verified.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public static bool TryParse(
return true;
}

refined = default!;
refined = default;
failureMessage = $"The boolean value must be 'True', instead found '{value}'";
return false;
}
Expand Down Expand Up @@ -112,4 +112,20 @@ public override int GetHashCode()
{
return HashCode.Combine(_value);
}

/// <summary>
/// Returns the string representation of the underlying bool
/// </summary>
public override string ToString()
{
return Value.ToString() ?? string.Empty;
}

/// <summary>
/// Returns the string representation of the underlying bool
/// </summary>
public string ToString(IFormatProvider? provider)
{
return ((IConvertible)Value).ToString(provider) ?? string.Empty;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public static bool TryParse(
return true;
}

refined = default!;
refined = default;
failureMessage = fm;
return false;
}
Expand Down Expand Up @@ -112,4 +112,20 @@ public override int GetHashCode()
{
return HashCode.Combine(_value);
}

/// <summary>
/// Returns the string representation of the underlying bool
/// </summary>
public override string ToString()
{
return Value.ToString() ?? string.Empty;
}

/// <summary>
/// Returns the string representation of the underlying bool
/// </summary>
public string ToString(IFormatProvider? provider)
{
return ((IConvertible)Value).ToString(provider) ?? string.Empty;
}
}
Loading
Loading