Skip to content

Commit

Permalink
feat: add analyser support to explicit conversions
Browse files Browse the repository at this point in the history
  • Loading branch information
bmazzarol committed Jan 5, 2025
1 parent 0ac08d4 commit 6127d41
Show file tree
Hide file tree
Showing 15 changed files with 521 additions and 240 deletions.
2 changes: 1 addition & 1 deletion Tuxedo.Docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ partial struct that will eventually be the refined type.

So in this case that type will be called a `PasswordString`, it looks like this,

[!code-csharp[Example1](../Tuxedo.Tests/PasswordStringExample.cs#ExampleRefinement)]
[!code-csharp[](../Tuxedo.Tests/PasswordStringExample.cs#ExampleRefinement)]

Which can be used like this,

Expand Down
5 changes: 2 additions & 3 deletions Tuxedo.Docs/rules/TUX001.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,11 @@ public class Test
{
public void TestMethod()
{
// this is not compliant
// these are not compliant
TrueBool tb1 = default;
// or this
var tb2 = default(TrueBool);

// these are all valid
// these are
var tb3 = (TrueBool) true;
var tb4 = TrueBool.Parse(true);

Expand Down
7 changes: 3 additions & 4 deletions Tuxedo.Docs/rules/TUX002.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@ public class Test
{
public void TestMethod()
{
// this is not compliant
// these are not compliant
TrueBool tb1 = new();
// or this
var tb2 = new TrueBool();

// these are all valid
var tb3 = (TrueBool) true;
// these are
var tb3 = (TrueBool)true;
var tb4 = TrueBool.Parse(true);

if(TrueBool.TryParse(true, out TrueBool tb5, out _))
Expand Down
12 changes: 8 additions & 4 deletions Tuxedo.Docs/rules/TUX003.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ public class Test
{
public void TestMethod()
{
// this is not compliant
var tb1 = TrueBool.Parse(false);
// thes are not compliant
var v1 = TrueBool.Parse(false);
var v2 = (TrueBool)false;
var v3 = TrueBool.TryParse(false, out _, out _);

// this is
var tb2 = TrueBool.Parse(true);
// these are
var v4 = TrueBool.Parse(true);
var v5 = (TrueBool)true;
var v6 = TrueBool.TryParse(true, out _, out _);
}
}
```
28 changes: 28 additions & 0 deletions Tuxedo.Examples/Examples.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Tuxedo;

namespace Examples;

/// <summary>
/// An int that must be positive
/// </summary>
public readonly partial struct PositiveInt
{
[Refinement("Must be positive", Name = nameof(PositiveInt))]
private static bool IsPositive(int value) => value > 0;
}

/// <summary>
/// An array that always has at least 1 value
/// </summary>
/// <typeparam name="T">some T</typeparam>
public readonly partial struct NonEmptyArray<T>
{
[Refinement("Must be not empty", Name = "NonEmptyArray")]
private static bool IsNotEmpty<T>(T[] value) => value.Length > 0;

Check warning on line 21 in Tuxedo.Examples/Examples.cs

View workflow job for this annotation

GitHub Actions / Publish to Nuget

Type parameter 'T' has the same name as the type parameter from outer type 'NonEmptyArray<T>'

public PositiveInt Length => (PositiveInt)Value.Length;

public T Head => Value[0];

public T[] Rest => Value[1..];
}
33 changes: 33 additions & 0 deletions Tuxedo.Examples/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Diagnostics;
using Examples;

Console.WriteLine("Welcome to Tuxedo!");

// only positive integers work
#pragma warning disable TUX003 // this is required otherwise it will not compile
_ = (PositiveInt)(-1); // this throws an ArgumentOutOfRange exception at runtime and will not compile if TUX003 is enabled
#pragma warning restore TUX003
// this is fine
var v2 = (PositiveInt)1;
// all default creation paths are also disallowed
#pragma warning disable TUX001 // this is required otherwise it will not compile
_ = default(PositiveInt);
#pragma warning restore TUX001
#pragma warning disable TUX002 // this is required otherwise it will not compile
_ = new PositiveInt();
#pragma warning restore TUX002

// we can also create more advanced types like this non-empty array
var nea = NonEmptyArray<PositiveInt>.Parse([v2]);

// this type has a positive length and can always access head
int length = nea.Length; // implicit conversion back to raw type
int head = nea.Head;

Debug.Assert(head == length);

// if fail fast is not your vibe, then use the TryParse methods
if (NonEmptyArray<PositiveInt>.TryParse([v2], out var v5, out _))
{
Debug.Assert(nea.Equals(v5));
}
17 changes: 17 additions & 0 deletions Tuxedo.Examples/Tuxedo.Examples.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Tuxedo.SourceGenerator\Tuxedo.SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>

<ItemGroup>
<AssemblyAttribute Include="System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute"/>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Collections.Concurrent;
using System.Reflection;
using Microsoft.CodeAnalysis;
using Tuxedo.SourceGenerator.Extensions;

namespace Tuxedo.SourceGenerator.Analysers;

public sealed partial class InvalidConstAssignmentAnalyser
{
private Lazy<RefinementService?>? _refinementService;

/// <summary>
/// Provides access to the runtime refinement predicates for refined types
/// </summary>
private sealed class RefinementService(Type runtimeRefinementServiceType)
{
private readonly ConcurrentDictionary<string, Func<object, string?>?> _refinementDelegates =
new(StringComparer.Ordinal);

public static RefinementService? Build(Compilation compilation, CancellationToken token)
{
using var ms = new MemoryStream();
var result = compilation.Emit(ms, cancellationToken: token);

if (!result.Success)
{
return null;
}

ms.Seek(0, SeekOrigin.Begin);
#pragma warning disable RS1035 // we need to load it here, as we want to run code against the current state of the compilation
var assembly = Assembly.Load(ms.ToArray());
#pragma warning restore RS1035
var type = assembly.GetType("Tuxedo.RefinementService");

return new RefinementService(type);
}

public string? TestAgainst(string refinedTypeName, object value)
{
var refinementDelegate = _refinementDelegates.GetOrAdd(
refinedTypeName,
BuildRefinementDelegate
);

return refinementDelegate?.Invoke(value);
}

private Func<object, string?>? BuildRefinementDelegate(string fn)
{
var methodInfo = runtimeRefinementServiceType.GetMethod(
$"TestAgainst{fn.RemoveNamespace()}",
#pragma warning disable S3011
BindingFlags.NonPublic | BindingFlags.Static
#pragma warning restore S3011
);

if (methodInfo == null)
{
return null;
}

return (Func<object, string?>?)methodInfo.CreateDelegate(typeof(Func<object, string?>));
}
}
}
Loading

0 comments on commit 6127d41

Please sign in to comment.