-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add analyser support to explicit conversions
- Loading branch information
Showing
15 changed files
with
521 additions
and
240 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
|
||
public PositiveInt Length => (PositiveInt)Value.Length; | ||
|
||
public T Head => Value[0]; | ||
|
||
public T[] Rest => Value[1..]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
66 changes: 66 additions & 0 deletions
66
Tuxedo.SourceGenerator/Analysers/InvalidConstAssignmentAnalyser.RefinementService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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?>)); | ||
} | ||
} | ||
} |
Oops, something went wrong.