Skip to content

Commit

Permalink
Merge pull request #19 from bmazzarol/feat/implicit-option
Browse files Browse the repository at this point in the history
feat: support for implicit conversion from raw to refined
  • Loading branch information
bmazzarol authored Jan 8, 2025
2 parents fb373d2 + fcf8530 commit 9d44c1f
Show file tree
Hide file tree
Showing 25 changed files with 592 additions and 55 deletions.
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;
}
}
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>
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
18 changes: 17 additions & 1 deletion Tuxedo.Tests/BoolRefinementsTests.Case4#FalseBool.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 '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;
}
}
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

0 comments on commit 9d44c1f

Please sign in to comment.