Skip to content

Commit

Permalink
feat: support internal modifier
Browse files Browse the repository at this point in the history
  • Loading branch information
bmazzarol committed Sep 8, 2024
1 parent a448e71 commit 8849e66
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Linq;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace Tuxedo.SourceGenerators;

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

public RefinementAttributeParts(MethodDeclarationSyntax methodDeclaration)
{
var arguments = methodDeclaration
.AttributeLists.SelectMany(list => list.Attributes)
.Single(attribute => attribute.Name.ToString() == "Refinement")
.ArgumentList!.Arguments;

FailureMessage = arguments[0].Expression.ToString();
IsInternal = arguments.Count > 1 && arguments[1].Expression.ToString() == "false";
}
}
}
65 changes: 38 additions & 27 deletions Tuxedo.SourceGenerators/RefinementSourceGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
namespace Tuxedo.SourceGenerators;

[Generator]
public sealed class RefinementSourceGenerator : IIncrementalGenerator
public sealed partial class RefinementSourceGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
Expand Down Expand Up @@ -56,36 +56,26 @@ ImmutableArray<MethodDeclarationSyntax> methodDeclarations
var model = compilation.GetSemanticModel(methodDeclaration.SyntaxTree);
var methodSymbol = model.GetDeclaredSymbol(methodDeclaration)!;
var @namespace = methodSymbol.ContainingNamespace.ToDisplayString();
var @class = methodSymbol.ContainingType.ToDisplayString();
var containingType = methodSymbol.ContainingType;
var @class = containingType.ToDisplayString();
var name = methodDeclaration.Identifier.Text;
var failureMessage = methodDeclaration
.AttributeLists.SelectMany(als => als.Attributes)
.First(a => a.Name.ToString() == "Refinement")
.ArgumentList!.Arguments.First()
.Expression.ToString();

var attributeParts = new RefinementAttributeParts(methodDeclaration);

// get the first parameter type
var parameterTypeSemanticModel = compilation.GetSemanticModel(
methodDeclaration.ParameterList.Parameters.First().Type!.SyntaxTree
);
var parameterType = parameterTypeSemanticModel
.GetTypeInfo(methodDeclaration.ParameterList.Parameters.First().Type!)
var firstParam = methodDeclaration.ParameterList.Parameters.First().Type!;
var firstParamSemanticModel = compilation.GetSemanticModel(firstParam.SyntaxTree);
var parameterType = firstParamSemanticModel
.GetTypeInfo(firstParam)
.Type!.ToDisplayString();

// get the generic type arguments
var genericTypeArguments = methodSymbol.TypeArguments;
var isGeneric = genericTypeArguments.Length > 0;
var generics = isGeneric
? $"<{string.Join(", ", methodSymbol.TypeArguments.Select(t => t.ToDisplayString()))}>"
: string.Empty;

// get the constraints for the generic type arguments
var genericTypeConstraints = isGeneric
? string.Join(
"\n",
methodDeclaration.ConstraintClauses.Select(x => x.ToString()).ToArray()
)
: string.Empty;
ExtractGenericPartDetails(
methodSymbol,
methodDeclaration,
out var generics,
out var genericTypeConstraints
);

var source = $$"""
// <auto-generated/>
Expand All @@ -99,7 +89,7 @@ namespace {{@namespace}};
/// <summary>
/// Refinement implementation for {{@class}}.{{name}}
/// </summary>
public readonly partial struct {{name}}{{generics}} : IRefinement<{{name}}{{generics}}, {{parameterType}}>
{{attributeParts.AccessModifier}} readonly partial struct {{name}}{{generics}} : IRefinement<{{name}}{{generics}}, {{parameterType}}>
{{genericTypeConstraints}}
{
/// <inheritdoc />
Expand All @@ -110,7 +100,7 @@ public bool CanBeRefined({{parameterType}} value, [NotNullWhen(false)] out strin
failureMessage = null;
return true;
}
failureMessage = ${{failureMessage}};
failureMessage = ${{attributeParts.FailureMessage}};
return false;
}
}
Expand All @@ -122,4 +112,25 @@ public bool CanBeRefined({{parameterType}} value, [NotNullWhen(false)] out strin
);
}
}

private static void ExtractGenericPartDetails(
IMethodSymbol methodSymbol,
MethodDeclarationSyntax methodDeclaration,
out string generics,
out string constraints
)
{
generics = string.Empty;
constraints = string.Empty;

var genericTypeArguments = methodSymbol.TypeArguments;
if (genericTypeArguments.Length == 0)
{
return;
}

generics = $"<{string.Join(", ", genericTypeArguments.Select(t => t.ToDisplayString()))}>";
var parts = methodDeclaration.ConstraintClauses.Select(x => x.ToString());
constraints = string.Join("\n", parts);
}
}
25 changes: 25 additions & 0 deletions Tuxedo.Tests/CustomAccessModifierTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using FluentAssertions;
using Xunit;

namespace Tuxedo.Tests;

public static class CustomAccessModifier
{
[Refinement("`{value}` is not a whitespace character", isPublic: false)]
public static bool IsWhiteSpace(char value) => char.IsWhiteSpace(value);
}

public sealed class CustomAccessModifierTests
{
private static string AppendWhiteSpace(char value, Refined<char, IsWhiteSpace> ws) =>
$"{value} {ws.Value}";

[Fact(DisplayName = "Internal refinements can be used in the same assembly")]
public void Case1()
{
const char value = ' ';
Refined<char, IsWhiteSpace> ws = value;
AppendWhiteSpace(value, ws).Should().Be(" ");
typeof(IsWhiteSpace).IsPublic.Should().BeFalse();
}
}
40 changes: 40 additions & 0 deletions Tuxedo.Tests/GenericRefinementTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using FluentAssertions;
using Xunit;

namespace Tuxedo.Tests;

public static class GenericRefinements
{
[Refinement("The value must be '{default(TOther).Value}', instead found '{value}'")]
internal static bool Equal<T, TOther>(T value)
where TOther : struct, IConstant<TOther, T> => Equals(value, default(TOther).Value);
}

public readonly record struct FortyTwo : IConstant<FortyTwo, int>
{
public int Value => 42;
}

public sealed class GenericRefinementTests
{
[Fact(DisplayName = "A value can be refined to a constant value")]
public void Case1()
{
const int value = 42;
Refined<int, Equal<int, FortyTwo>> refined = value;
refined.Value.Should().Be(42);
}

[Fact(
DisplayName = "A value that is not equal to the constant value should fail the refinement"
)]
public void Case2()
{
const int value = 43;
Assert
.Throws<RefinementFailureException>(() => (Refined<int, Equal<int, FortyTwo>>)value)
.Message.Should()
.Be("The value must be '42', instead found '43'");
Refined.TryRefine(value, out Refined<int, Equal<int, FortyTwo>> _).Should().BeFalse();
}
}
10 changes: 8 additions & 2 deletions Tuxedo/RefinementAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,19 @@ public sealed class RefinementAttribute : Attribute
/// The message to display when the refinement fails.
/// The `value` parameter is available for string interpolation.
/// </summary>
public string FailureMessage { get; set; }
public string FailureMessage { get; }

/// <summary>
/// Indicates whether the refinement is public
/// </summary>
public bool IsPublic { get; }

/// <summary>
/// Initializes a new instance of the <see cref="RefinementAttribute"/> class.
/// </summary>
public RefinementAttribute(string failureMessage)
public RefinementAttribute(string failureMessage, bool isPublic = true)
{
FailureMessage = failureMessage;
IsPublic = isPublic;
}
}

0 comments on commit 8849e66

Please sign in to comment.