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 1 commit
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
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,6 +37,8 @@ private static string RenderMultiRefinedType(RefinedTypeDetails model)

{{RenderEqualityMembers(model)}}

{{FormattingMembers}}

/// <summary>
/// Standard deconstruction to the underlying values
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ private readonly record struct RefinedTypeDetails(
string? GenericConstraints,
string? RawType,
string? RefinedType,
string? AlternativeType
string? AlternativeType,
bool HasImplicitConversionFromRaw
)
{
public string? RefinedTypeXmlSafeName => (RefinedType + Generics).EscapeXml();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ private readonly ref struct RefinementAttributeParts
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)}}

{{FormattingMembers}}
}
""";
}
Expand Down Expand Up @@ -103,12 +105,16 @@ private static string RenderParseMethod(RefinedTypeDetails model)
{
return $$"""
/// <summary>
/// Explicit conversion from a {{model.RawType.EscapeXml()}} to a {{model.RefinedTypeXmlSafeName}}
/// {{(
model.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.HasImplicitConversionFromRaw ? "implicit" : "explicit"
)}} operator {{model.RefinedType}}{{model.Generics}}({{model.RawType}} value)
{
return Parse(value);
}
Expand Down Expand Up @@ -199,4 +205,12 @@ public override int GetHashCode()
}
""";
}

private const string FormattingMembers = """
/// <inheritdoc />
public override string? ToString()
{
return _value?.ToString();
}
""";
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ CancellationToken token
var attributeParts = new RefinementAttributeParts(methodDeclarationSyntax);
var failureMessage = attributeParts.FailureMessage;
var accessModifier = attributeParts.AccessModifier;
var hasImplicitConversionFromRaw = attributeParts.HasImplicitConversionFromRaw;

// extract the generic parts
ExtractGenericPartDetails(
Expand Down Expand Up @@ -139,7 +140,8 @@ attributeParts.Name is not null
GenericConstraints: genericTypeConstraints,
RawType: rawType,
RefinedType: refinedType,
AlternativeType: altType
AlternativeType: altType,
HasImplicitConversionFromRaw: hasImplicitConversionFromRaw
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,10 @@ public override int GetHashCode()
{
return HashCode.Combine(_value);
}

/// <inheritdoc />
public override string? ToString()
{
return _value?.ToString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,10 @@ public override int GetHashCode()
{
return HashCode.Combine(_value);
}

/// <inheritdoc />
public override string? ToString()
{
return _value?.ToString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,10 @@ public override int GetHashCode()
{
return HashCode.Combine(_value);
}

/// <inheritdoc />
public override string? ToString()
{
return _value?.ToString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//HintName: Case4.g.cs
// <auto-generated/>
#nullable enable

using System;
using System.Diagnostics.CodeAnalysis;
using Tuxedo;

namespace <global namespace>;

/// <summary>
/// A refined bool based on the Test.ExampleWithImplicitConversion refinement predicate
/// </summary>
[RefinedType]
public readonly partial struct Case4 : IEquatable<Case4>
{
private readonly bool? _value;

/// <summary>
/// The underlying bool
/// </summary>
public bool Value => _value ?? throw new InvalidOperationException("Do not use the default value, please use the Parse and TryParse methods to construct a Case4");

/// <summary>
/// Implicit conversion from the Case4 to a bool
/// </summary>
/// <param name="this">the Case4</param>
/// <returns>underlying bool</returns>
public static implicit operator bool(Case4 @this)
{
return @this.Value;
}

private Case4(bool value)
{
_value = value;
}

/// <summary>
/// Implicit conversion from a bool to a Case4
/// </summary>
/// <param name="value">raw bool</param>
/// <returns>refined Case4</returns>
/// <exception cref="ArgumentOutOfRangeException">if the Test.ExampleWithImplicitConversion refinement fails</exception>
public static implicit operator Case4(bool value)
{
return Parse(value);
}

/// <summary>
/// Refines the bool or throws
/// </summary>
/// <param name="value">raw bool</param>
/// <returns>refined Case4</returns>
/// <exception cref="ArgumentOutOfRangeException">if the Test.ExampleWithImplicitConversion refinement fails</exception>
public static Case4 Parse(bool value)
{
return TryParse(value, out var result, out var failureMessage) ? result : throw new ArgumentOutOfRangeException(nameof(value), value, failureMessage);
}

/// <summary>
/// Try and refine the bool against the Test.ExampleWithImplicitConversion refinement
/// </summary>
/// <param name="value">raw bool</param>
/// <param name="refined">refined Case4 when true</param>
/// <param name="failureMessage">error message when false</param>
/// <returns>true if refined, false otherwise</returns>
public static bool TryParse(
bool value,
out Case4 refined,
[NotNullWhen(false)] out string? failureMessage
)
{
if (Test.ExampleWithImplicitConversion(value))
{
refined = new Case4(value);
failureMessage = null;
return true;
}

refined = default!;
bmazzarol marked this conversation as resolved.
Show resolved Hide resolved
failureMessage = $"Must be true";
return false;
}

// <inheritdoc />
public bool Equals(Case4 other)
{
return Nullable.Equals(_value, other._value);
}

/// <inheritdoc />
public override bool Equals(object? obj)
{
return obj is Case4 other && Equals(other);
}

/// <inheritdoc />
public static bool operator ==(Case4 left, Case4 right)
{
return left.Equals(right);
}

/// <inheritdoc />
public static bool operator !=(Case4 left, Case4 right)
{
return !(left == right);
}

/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(_value);
}

/// <inheritdoc />
public override string? ToString()
{
return _value?.ToString();
}
bmazzarol marked this conversation as resolved.
Show resolved Hide resolved
}
16 changes: 16 additions & 0 deletions Tuxedo.Tests/ConfigureRefinementTypesExample.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ public sealed class ConfigureRefinementTypesExample
public static string? ExampleWithReturnedFailureMessage(bool value) =>
!value ? "Must be true" : null;

[Refinement(
Name = nameof(Case4),
HasImplicitConversionFromRaw = true,
FailureMessage = "Must be true"
)]
public static bool ExampleWithImplicitConversion(bool value) => value;

[Fact(DisplayName = "A string returning refinement method renders correctly")]
public Task Case1()
{
Expand All @@ -23,4 +30,13 @@ public Task Case1()
!value ? "Must be true" : null;
""".VerifyRefinement();
}

[Fact(DisplayName = "A refinement type can be created via implicit conversion")]
public Task Case2()
{
return """
[Refinement("Must be true", Name = nameof(Case4), HasImplicitConversionFromRaw = true)]
public static bool ExampleWithImplicitConversion(bool value) => value;
""".VerifyRefinement();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,10 @@ public override int GetHashCode()
{
return HashCode.Combine(_value);
}

/// <inheritdoc />
public override string? ToString()
{
return _value?.ToString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,10 @@ public override int GetHashCode()
{
return HashCode.Combine(_value);
}

/// <inheritdoc />
public override string? ToString()
{
return _value?.ToString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ public override int GetHashCode()
return HashCode.Combine(_value, _altValue);
}

/// <inheritdoc />
public override string? ToString()
{
return _value?.ToString();
}

/// <summary>
/// Standard deconstruction to the underlying values
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions Tuxedo.Tests/GuidStringTests.Case3#GuidString.g.verified.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ public override int GetHashCode()
return HashCode.Combine(_value, _altValue);
}

/// <inheritdoc />
public override string? ToString()
{
return _value?.ToString();
}

/// <summary>
/// Standard deconstruction to the underlying values
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions Tuxedo.Tests/OddNumberExample.Case4#Odd.g.verified.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,10 @@ public override int GetHashCode()
{
return HashCode.Combine(_value);
}

/// <inheritdoc />
public override string? ToString()
{
return _value?.ToString();
}
}
6 changes: 6 additions & 0 deletions Tuxedo.Tests/OddNumberExample.Case5#Odd.g.verified.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,10 @@ public override int GetHashCode()
{
return HashCode.Combine(_value);
}

/// <inheritdoc />
public override string? ToString()
{
return _value?.ToString();
}
}
Loading
Loading