diff --git a/CSharpSourceBuilder.TestApplication/Program.cs b/CSharpSourceBuilder.TestApplication/Program.cs
index cd0d93f..551df15 100644
--- a/CSharpSourceBuilder.TestApplication/Program.cs
+++ b/CSharpSourceBuilder.TestApplication/Program.cs
@@ -1,6 +1,7 @@
// See https://aka.ms/new-console-template for more information
using RhoMicro.CodeAnalysis;
+using RhoMicro.CodeAnalysis.Lyra;
using var builder = new CSharpSourceBuilder();
diff --git a/Directory.Build.props b/Directory.Build.props
index eba16d4..b4a0e28 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -3,11 +3,12 @@
true
All
true
- latest
+ preview
enable
enable
True
true
+ RhoMicro.CodeAnalysis
$(SolutionName)
$(SolutionName).$(MSBuildProjectName)
@@ -17,7 +18,7 @@
README.md
MPL-2.0
Paul Braetz
- 2024
+ $([System.DateTime]::Now.ToString('yyyy'))
RhoMicro
$(SolutionName).$(MSBuildProjectName)
https://github.com/SleepWellPupper/RhoMicro.CodeAnalysis/tree/master/$(MSBuildProjectName)
diff --git a/DslGenerator.TestApp/DslGenerator.TestApp.csproj b/DslGenerator.TestApp/DslGenerator.TestApp.csproj
index 8f47357..c3ded4c 100644
--- a/DslGenerator.TestApp/DslGenerator.TestApp.csproj
+++ b/DslGenerator.TestApp/DslGenerator.TestApp.csproj
@@ -22,10 +22,11 @@
-
+
+
-
\ No newline at end of file
+
diff --git a/DslGenerator/DslGenerator.csproj b/DslGenerator/DslGenerator.csproj
index 9fe7d37..d66c0b0 100644
--- a/DslGenerator/DslGenerator.csproj
+++ b/DslGenerator/DslGenerator.csproj
@@ -1,55 +1,59 @@
-
-
-
- netstandard2.0
- false
- true
- true
- true
- enable
- enable
-
-
-
-
-
-
-
- true
- true
- Generates utilities for lexing and parsing domain specific languages, little languages etc.
- Source Generator
-
-
-
- $(DefineConstants);DSL_GENERATOR
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ netstandard2.0
+ false
+ true
+ true
+ true
+ enable
+ enable
+
+
+
+
+
+
+
+ true
+ true
+ Generates utilities for lexing and parsing domain specific languages, little languages etc.
+ Source Generator
+
+
+
+ $(DefineConstants);DSL_GENERATOR
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DslGenerator/Lexing/Lexeme.cs b/DslGenerator/Lexing/Lexeme.cs
index dede6dc..75b33a8 100644
--- a/DslGenerator/Lexing/Lexeme.cs
+++ b/DslGenerator/Lexing/Lexeme.cs
@@ -8,36 +8,37 @@ namespace RhoMicro.CodeAnalysis.DslGenerator.Lexing;
[IncludeFile]
#endif
[UnionType]
-[UnionTypeSettings(
- ToStringSetting = ToStringSetting.Simple,
- Miscellaneous = MiscellaneousSettings.Default | MiscellaneousSettings.EmitGeneratedSourceCode)]
+[UnionTypeSettings(ToStringSetting = ToStringSetting.Simple)]
internal readonly partial struct Lexeme : IEquatable, IEquatable, IEquatable
{
- public Int32 Length => Match(
- s => s.Length,
- s => 1,
- s => s.Length);
+ public Int32 Length => Switch(
+ onString: s => s.Length,
+ onChar: s => 1,
+ onStringSlice: s => s.Length);
+
public static Lexeme Empty { get; } = String.Empty;
- public Boolean Equals(Lexeme other) =>
- Match(other.Equals, other.Equals, other.Equals);
- public override Int32 GetHashCode() =>
- Match(v => v.GetHashCode(), v => v.GetHashCode(), v => v.GetHashCode());
+
+ public Boolean Equals(Lexeme other) => Switch(other.Equals, other.Equals, other.Equals);
+
public Boolean Equals(Char c) =>
- Match(
- s => s.Length == 1 && s[0] == c,
- thisChar => thisChar == c,
- s => s.Equals(c));
+ Switch(
+ onString: s => s.Length == 1 && s[0] == c,
+ onChar: thisChar => thisChar == c,
+ onStringSlice: s => s.Equals(c));
+
public Boolean Equals(String s) =>
- Match(
- thisString => thisString == s,
- c => s.Length == 1 && s[0] == c,
- slice => slice.Equals(s));
+ Switch(
+ onString: thisString => thisString == s,
+ onChar: c => s.Length == 1 && s[0] == c,
+ onStringSlice: slice => slice.Equals(s));
+
public Boolean Equals(StringSlice s) =>
- Match(s.Equals, s.Equals, s.Equals);
+ Switch(s.Equals, s.Equals, s.Equals);
+
public String ToEscapedString() =>
ToString()?
- .Replace("\n", "\\n")
- .Replace("\r", "\\r")
- .Replace("\t", "\\t")
- ?? String.Empty;
+ .Replace("\n", "\\n")
+ .Replace("\r", "\\r")
+ .Replace("\t", "\\t")
+ ?? String.Empty;
}
diff --git a/DslGenerator/Lexing/SourceText.cs b/DslGenerator/Lexing/SourceText.cs
index 66090bc..a7a3d99 100644
--- a/DslGenerator/Lexing/SourceText.cs
+++ b/DslGenerator/Lexing/SourceText.cs
@@ -12,15 +12,15 @@ namespace RhoMicro.CodeAnalysis.DslGenerator.Lexing;
internal readonly partial struct SourceText : IDisposable
{
public String ToString(CancellationToken cancellationToken) =>
- Match(
- s => s,
- s =>
+ Switch(
+ onString: s => s,
+ onStream: s =>
{
cancellationToken.ThrowIfCancellationRequested();
var reader = new StreamReader(s);
var resultBuilder = new StringBuilder();
var line = reader.ReadLine();
- while(line != null)
+ while (line != null)
{
cancellationToken.ThrowIfCancellationRequested();
_ = resultBuilder.AppendLine(line);
@@ -31,10 +31,12 @@ public String ToString(CancellationToken cancellationToken) =>
return result;
});
+
public static SourceText Empty { get; } = String.Empty;
+
public void Dispose()
{
- if(TryAsStream(out var s))
+ if (TryCastToStream(out var s))
s.Dispose();
}
}
diff --git a/DslGenerator/Lexing/Tokenizer.cs b/DslGenerator/Lexing/Tokenizer.cs
index f319888..17f2521 100644
--- a/DslGenerator/Lexing/Tokenizer.cs
+++ b/DslGenerator/Lexing/Tokenizer.cs
@@ -5,24 +5,24 @@ namespace RhoMicro.CodeAnalysis.DslGenerator.Lexing;
using static RhoMicro.CodeAnalysis.DslGenerator.Analysis.DiagnosticDescriptors;
using RhoMicro.CodeAnalysis.DslGenerator.Grammar;
using RhoMicro.CodeAnalysis.DslGenerator.Analysis;
-
using static Lexemes;
#if DSL_GENERATOR
[IncludeFile]
internal
#endif
-partial class Tokenizer
+ partial class Tokenizer
{
[UnionType]
private readonly partial struct TokenOrType;
+
public static Tokenizer Instance { get; } = new();
public TokenizeResult Tokenize(SourceText sourceText, CancellationToken cancellationToken
#if DSL_GENERATOR
- , String filePath = ""
+ , String filePath = ""
#endif
- )
+ )
{
cancellationToken.ThrowIfCancellationRequested();
@@ -32,7 +32,7 @@ public TokenizeResult Tokenize(SourceText sourceText, CancellationToken cancella
var isUnknown = false;
var (start, current, line, character) = (0, 0, 0, 0);
- while(!isAtEnd())
+ while (!isAtEnd())
{
cancellationToken.ThrowIfCancellationRequested();
scanToken();
@@ -45,7 +45,7 @@ public TokenizeResult Tokenize(SourceText sourceText, CancellationToken cancella
void scanToken()
{
var c = advance();
- switch(c)
+ switch (c)
{
case Equal:
addToken(TokenType.Equal);
@@ -58,9 +58,7 @@ void scanToken()
break;
case Alternative:
//check for incremental alternative "/="
- var type = match(Equal) ?
- TokenType.SlashEqual :
- TokenType.Slash;
+ var type = match(Equal) ? TokenType.SlashEqual : TokenType.Slash;
addToken(type);
break;
case GroupOpen:
@@ -92,7 +90,7 @@ void scanToken()
break;
case CarriageReturn:
closeUnknown();
- if(lookAhead() == NewLine)
+ if (lookAhead() == NewLine)
advancePure();
addNewLine();
break;
@@ -103,13 +101,15 @@ void scanToken()
consumeWhitespace(Tab);
break;
default:
- if(isAlpha(c))
+ if (isAlpha(c))
{
name();
- } else if(isDigit(c))
+ }
+ else if (isDigit(c))
{
specificRepetition();
- } else
+ }
+ else
{
openUnknown();
}
@@ -138,7 +138,7 @@ void specificRepetition()
{
closeUnknown();
- while(isDigit(lookAhead()))
+ while (isDigit(lookAhead()))
advancePure();
addToken(TokenType.Number);
}
@@ -159,14 +159,14 @@ void regressPure()
void closeUnknown()
{
- if(isUnknown)
+ if (isUnknown)
{
- if(!isAtEnd())
+ if (!isAtEnd())
regressPure();
isUnknown = false;
addToken(TokenType.Unknown);
diagnostics!.Add(UnexpectedCharacter, getLocation());
- if(!isAtEnd())
+ if (!isAtEnd())
advancePure();
}
}
@@ -177,9 +177,9 @@ void addToken(TokenOrType tokenOrType)
{
closeUnknown();
- var token = tokenOrType.Match(
- token => token,
- type => new Token(type, getLexeme(), getLocation()));
+ var token = tokenOrType.Switch(
+ onToken: token => token,
+ onTokenType: type => new Token(type, getLexeme(), getLocation()));
tokens!.Add(token);
resetLexemeStart();
}
@@ -196,9 +196,11 @@ void discardToken()
Lexeme getLexeme() => new StringSlice(source!, start, current - start);
- Char? lookAhead(Int32 lookAheadOffset = 0) => current + lookAheadOffset >= source!.Length ? null : source![current + lookAheadOffset];
+ Char? lookAhead(Int32 lookAheadOffset = 0) =>
+ current + lookAheadOffset >= source!.Length ? null : source![current + lookAheadOffset];
- Char? lookBehind(Int32 lookBehindOffset = 0) => current - lookBehindOffset < 1 ? null : source![current - 1 - lookBehindOffset];
+ Char? lookBehind(Int32 lookBehindOffset = 0) =>
+ current - lookBehindOffset < 1 ? null : source![current - 1 - lookBehindOffset];
Location getLocation() => Location.Create(
line,
@@ -207,11 +209,11 @@ Location getLocation() => Location.Create(
#if DSL_GENERATOR
, filePath
#endif
- );
+ );
Boolean match(Char expected)
{
- if(isAtEnd() || source![current] != expected)
+ if (isAtEnd() || source![current] != expected)
return false;
current++;
return true;
@@ -230,13 +232,13 @@ void comment()
discardToken(); //discard hash token
advancePure(); //consume hash
- if(isAtNewLine() || isAtEnd())
+ if (isAtNewLine() || isAtEnd())
return;
- while(!isAtNewLine() && !isAtEnd(lookaheadOffset: 1))
+ while (!isAtNewLine() && !isAtEnd(lookaheadOffset: 1))
advancePure();
- if(!isAtNewLine())
+ if (!isAtNewLine())
advancePure(); //consume last comment char
addToken(TokenType.Comment);
@@ -246,7 +248,7 @@ void consumeWhitespace(Char expected)
{
closeUnknown();
- while(lookAhead() == expected)
+ while (lookAhead() == expected)
advancePure();
addToken(TokenType.Whitespace);
@@ -256,9 +258,9 @@ void terminal()
{
discardToken(); //discard quote token
var containsCharacters = false;
- while(( lookAhead() != Quote || lookBehind() == Escape ) && !isAtEnd())
+ while ((lookAhead() != Quote || lookBehind() == Escape) && !isAtEnd())
{
- if(lookAhead() == NewLine)
+ if (lookAhead() == NewLine)
{
line++;
}
@@ -267,19 +269,19 @@ void terminal()
containsCharacters = true;
}
- if(containsCharacters)
+ if (containsCharacters)
{
addToken(TokenType.Terminal);
}
- if(isAtEnd())
+ if (isAtEnd())
{
diagnostics.Add(UnterminatedTerminal, getLocation(), getLexeme());
return;
}
//add empty token
- if(!containsCharacters)
+ if (!containsCharacters)
{
addToken(TokenType.Terminal);
}
@@ -293,7 +295,7 @@ void name()
{
closeUnknown();
- while(isAlpha(lookAhead()))
+ while (isAlpha(lookAhead()))
advancePure();
addToken(TokenType.Name);
diff --git a/Equa/ComponentFactory.cs b/Equa/ComponentFactory.cs
new file mode 100644
index 0000000..4cc2f67
--- /dev/null
+++ b/Equa/ComponentFactory.cs
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Lyra;
+
+using Equa;
+using Library.Models.Collections;
+
+partial class ComponentFactory
+{
+
+ public static ListComponent, MemberModel, String> List(
+ EquatableList list,
+ Action append,
+ String separator = "",
+ String terminator = "")
+ => List, MemberModel>(
+ list,
+ append,
+ separator,
+ terminator);
+}
diff --git a/Equa/ContainingTypeModel.cs b/Equa/ContainingTypeModel.cs
new file mode 100644
index 0000000..3457eba
--- /dev/null
+++ b/Equa/ContainingTypeModel.cs
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Equa;
+
+using System.Collections.Immutable;
+
+internal readonly record struct ContainingTypeModel(
+ String Modifier,
+ String Name,
+ ImmutableArray TypeParameters)
+{
+ public Boolean Equals(ContainingTypeModel other)
+ {
+ if (other.Modifier != Modifier)
+ {
+ return false;
+ }
+
+ if (other.Name != Name)
+ {
+ return false;
+ }
+
+ if (!other.TypeParameters.SequenceEqual(TypeParameters))
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ public override Int32 GetHashCode()
+ {
+ var hc = new HashCode();
+ hc.Add(Modifier);
+ hc.Add(Name);
+
+ foreach (var typeParameter in TypeParameters)
+ {
+ hc.Add(typeParameter);
+ }
+
+ var result = hc.ToHashCode();
+
+ return result;
+ }
+}
diff --git a/Equa/Equa.csproj b/Equa/Equa.csproj
new file mode 100644
index 0000000..6533c42
--- /dev/null
+++ b/Equa/Equa.csproj
@@ -0,0 +1,45 @@
+
+
+
+ netstandard2.0
+ true
+ true
+ true
+ enable
+ true
+
+
+
+
+ true
+ true
+
+ Generates equality implementations that take into account value equality of collections.
+
+ Source Generator; Equality; Value Equality; Collection Equality
+
+
+
+ $(DefineConstants);RHOMICRO_CODEANALYSIS_EQUA
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
diff --git a/Equa/Generator.cs b/Equa/Generator.cs
new file mode 100644
index 0000000..4bfc1f3
--- /dev/null
+++ b/Equa/Generator.cs
@@ -0,0 +1,218 @@
+namespace RhoMicro.CodeAnalysis.Equa;
+
+using System.Collections.Immutable;
+using System.Runtime.InteropServices;
+using Generated;
+using Library.Models;
+using Lyra;
+using Microsoft.CodeAnalysis;
+
+///
+/// Generates collection aware value equality implementations for
+/// and .
+///
+[Generator(LanguageNames.CSharp)]
+public sealed partial class Generator : IIncrementalGenerator
+{
+ private static readonly SymbolDisplayFormat _namespaceFormat = new(
+ globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
+ typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces);
+
+ private static readonly CSharpSourceBuilderOptions _sourceBuilderOptions = new()
+ {
+ Prelude = static (b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.AppendLine(
+ $"""
+ //
+ // This file was generated using the Equa source generator.
+ // SPDX-License-Identifier: MPL-2.0
+ //
+ """);
+ }
+ };
+
+ ///
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var sourceProvider = context.SyntaxProvider.ForAttributeWithMetadataName(
+ typeof(ValueEqualityAttribute).FullName,
+ static (n, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ return true;
+ }, static (ctx, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (ctx.TargetSymbol is not { ContainingType: { } target })
+ {
+ return null;
+ }
+
+ using var collections = ModelCreationContext.CreateDefault(ct);
+
+ var containingTypes = collections.CollectionFactory.CreateList();
+ if (target.ContainingType is { } t)
+ {
+ appendContainingType(t);
+ }
+
+ var @namespace = target.ContainingNamespace.ToDisplayString(_namespaceFormat) ?? String.Empty;
+ var typeParameters = collections.CollectionFactory.CreateList();
+ var vectorMembers = collections.CollectionFactory.CreateList();
+ var scalarMembers = collections.CollectionFactory.CreateList();
+
+ for (var i = 0; i < target.TypeParameters.Length; i++)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ typeParameters[i] = target.TypeParameters[i].Name;
+ }
+
+ foreach (var member in target.GetMembers())
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (member is not
+ {
+ CanBeReferencedByName: true,
+ Kind: SymbolKind.Property or SymbolKind.Field,
+ IsStatic: false
+ })
+ {
+ continue;
+ }
+
+ var memberType = member switch
+ {
+ IFieldSymbol f => f.Type,
+ IPropertySymbol p => p.Type,
+ _ => null
+ };
+
+ if (memberType is null)
+ {
+ continue;
+ }
+
+ var memberModel = new MemberModel(
+ Type: memberType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
+ Name: member.Name);
+
+ if (ImplementsIEnumerable(memberType, ct))
+ {
+ vectorMembers.Add(memberModel);
+ }
+ else
+ {
+ scalarMembers.Add(memberModel);
+ }
+ }
+
+ var result = new RecordModel(
+ target.TypeKind,
+ target.IsRecord,
+ target.Name,
+ @namespace,
+ typeParameters,
+ containingTypes,
+ vectorMembers,
+ scalarMembers);
+
+ return result;
+
+ void appendContainingType(INamedTypeSymbol type)
+ {
+ if (type.ContainingType is { } t)
+ {
+ appendContainingType(t);
+ }
+
+ var typeParameters = new String[type.Arity];
+
+ for (var i = 0; i < type.TypeParameters.Length; i++)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ typeParameters[i] = type.TypeParameters[i].Name;
+ }
+
+ var containingType = new ContainingTypeModel(
+ $"{(type.IsRecord ? "record " : String.Empty)}{(type.TypeKind is TypeKind.Struct ? "struct" : "class")}",
+ type.Name,
+ ImmutableCollectionsMarshal.AsImmutableArray(typeParameters));
+
+ containingTypes.Add(containingType);
+ }
+ })!
+ .Where(m => m is not null)
+ .Select(static (m, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var builder = new CSharpSourceBuilder(_sourceBuilderOptions).SetCancellationToken(ct);
+
+ var hintName = builder
+ .SetCondition(m.Namespace is not [])
+ .Append($"{m.Namespace}.")
+ .UnsetCondition()
+ .Append(m.Name)
+ .SetCondition(m.TypeParameters is not [])
+ .Append($"`{m.TypeParameters.Count}")
+ .UnsetCondition()
+ .Append(".g.cs")
+ .ToString();
+
+ var source = builder.Clear().Append(new RecordComponent(m)).ToString();
+
+ return (hintName, source);
+ });
+
+ context.RegisterSourceOutput(sourceProvider, static (ctx, t) => ctx.AddSource(t.hintName, t.source));
+ IncludedFileSources.RegisterToContext(context);
+ }
+
+ private static Boolean ImplementsIEnumerable(ITypeSymbol type, CancellationToken ct)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ foreach (var @interface in type.AllInterfaces)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var isIEnumerable = @interface is
+ {
+ TypeKind: TypeKind.Interface,
+ Name: nameof(IEnumerable<>),
+ TypeArguments: [{ }],
+ ContainingNamespace:
+ {
+ Name: "Generic",
+ ContainingNamespace:
+ {
+ Name: "Collections",
+ ContainingNamespace:
+ {
+ Name: "System",
+ ContainingNamespace:
+ {
+ IsGlobalNamespace: true
+ }
+ }
+ }
+ }
+ };
+
+ if (isIEnumerable)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/Equa/MemberModel.cs b/Equa/MemberModel.cs
new file mode 100644
index 0000000..9894699
--- /dev/null
+++ b/Equa/MemberModel.cs
@@ -0,0 +1,5 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Equa;
+
+internal readonly record struct MemberModel(String Type, String Name);
diff --git a/Equa/Properties/launchSettings.json b/Equa/Properties/launchSettings.json
new file mode 100644
index 0000000..fd87e49
--- /dev/null
+++ b/Equa/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "Janus": {
+ "commandName": "DebugRoslynComponent",
+ "targetProject": "../Janus/Janus.csproj"
+ }
+ }
+}
diff --git a/Equa/README.md b/Equa/README.md
new file mode 100644
index 0000000..06f909c
--- /dev/null
+++ b/Equa/README.md
@@ -0,0 +1 @@
+# Equa
diff --git a/Equa/RecordComponent.cs b/Equa/RecordComponent.cs
new file mode 100644
index 0000000..4b6e254
--- /dev/null
+++ b/Equa/RecordComponent.cs
@@ -0,0 +1,175 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Equa;
+
+using Library.Models.Collections;
+using Lyra;
+using Microsoft.CodeAnalysis;
+
+internal readonly record struct RecordComponent(RecordModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var type = ComponentFactory.Create(Model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append("partial")
+ .SetCondition(m.IsRecord)
+ .Append(" record")
+ .UnsetCondition()
+ .Append(m.TypeKind is TypeKind.Struct ? " struct " : " class ")
+ .Append(new RecordNameComponent(m))
+ .SetCondition(m.IsRecord)
+ .Append(" : global::System.IEquatable<")
+ .Append(m.Name)
+ .SetCondition(m.TypeParameters is not [])
+ .Append($"<{ComponentFactory.List(m.TypeParameters, separator: ", ")}>")
+ .UnsetCondition()
+ .Append('>')
+ .UnsetCondition()
+ .AppendLine()
+ .Append($$"""
+ {
+ public override int GetHashCode()
+ {
+ var hc = new global::System.HashCode();
+
+ {{ComponentFactory.List(m.ScalarMembers, static (m, i, l, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"hc.Add(this.{m});");
+ }, "\n")}}
+
+ {{ComponentFactory.List(m.VectorMembers, static (m, i, l, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($$"""
+ foreach(var element in {{m}})
+ {
+ hc.Add(element);
+ }
+ """);
+ }, "\n")}}
+
+ var result = hc.ToHashCode();
+
+ return result;
+ }
+
+ {{ComponentFactory.Create(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (m.IsRecord)
+ {
+ return;
+ }
+
+ b.Append("public override bool Equals(object? other) => other is ")
+ .Append(new RecordNameComponent(m))
+ .Append(" o && Equals(o);");
+ })}}
+
+ {{ComponentFactory.Create(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (m.IsRecord)
+ {
+ return;
+ }
+
+ b.Append($$"""
+ public {{(m.IsRecord ? String.Empty : "override ")}}bool Equals({{new RecordNameComponent(m)}}{{(m.TypeKind is TypeKind.Struct ? String.Empty : "?")}} other)
+ {
+ {{ComponentFactory.Create(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (m.TypeKind is TypeKind.Struct)
+ {
+ return;
+ }
+
+ b.Append("""
+ if(other is null)
+ {
+ return false;
+ }
+ """
+ );
+ })}}
+
+ {{ComponentFactory.List(m.ScalarMembers, static (m, i, l, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($$"""
+ if(!global::System.Collections.Generic.EqualityComparer<{{m.Type}}>.Default.Equals(this.{{m.Name}}, other.{{m.Name}}))
+ {
+ return false;
+ }
+ """);
+ }, "\n")}}
+
+ {{ComponentFactory.List(m.VectorMembers, static (m, i, l, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($$"""
+ if(!this.{{m.Name}}.SequenceEqual(other.{{m.Name}}))
+ {
+ return false;
+ }
+ """);
+ }, "\n")}}
+
+ return true;
+ }
+ """
+ );
+ })}}
+ }
+ """
+ );
+ });
+
+ var containingTypes = ComponentFactory.Create((Model, type), static (t, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var (model, type) = t;
+
+ b.AppendLine("using System.Linq;");
+
+ foreach (var containingType in model.ContainingTypes)
+ {
+ b.Append($"{containingType.Modifier} {containingType.Name}");
+ if (containingType.TypeParameters is not [])
+ {
+ b.Append($"<{ComponentFactory.List(containingType.TypeParameters, separator: ", ")}>");
+ }
+
+ b.AppendLine().AppendLine('{').Indent();
+ }
+
+ b.AppendLine(type);
+
+ for (var i = 0; i < model.ContainingTypes.Count; i++)
+ {
+ b.Detent().AppendLine('}');
+ }
+ });
+
+ var @namespace = ComponentFactory.Namespace(
+ Model.Namespace,
+ containingTypes);
+
+ builder.Append(@namespace);
+ }
+}
diff --git a/Equa/RecordModel.cs b/Equa/RecordModel.cs
new file mode 100644
index 0000000..2b6a3ef
--- /dev/null
+++ b/Equa/RecordModel.cs
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Equa;
+
+using System.Collections.Immutable;
+using Library.Models.Collections;
+using Microsoft.CodeAnalysis;
+
+internal sealed record RecordModel(
+ TypeKind TypeKind,
+ Boolean IsRecord,
+ String Name,
+ String Namespace,
+ EquatableList TypeParameters,
+ EquatableList ContainingTypes,
+ EquatableList VectorMembers,
+ EquatableList ScalarMembers);
diff --git a/Equa/RecordNameComponent.cs b/Equa/RecordNameComponent.cs
new file mode 100644
index 0000000..43deb4c
--- /dev/null
+++ b/Equa/RecordNameComponent.cs
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Equa;
+
+using Lyra;
+
+internal readonly record struct RecordNameComponent(RecordModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ builder.Append(Model.Name)
+ .SetCondition(Model.TypeParameters is not [])
+ .Append($"<{ComponentFactory.List(Model.TypeParameters, separator: ", ")}>")
+ .UnsetCondition();
+ }
+}
diff --git a/Equa/ValueEqualityAttribute.cs b/Equa/ValueEqualityAttribute.cs
new file mode 100644
index 0000000..70950de
--- /dev/null
+++ b/Equa/ValueEqualityAttribute.cs
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis;
+
+using System;
+
+#if RHOMICRO_CODEANALYSIS_EQUA
+[RhoMicro.CodeAnalysis.IncludeFile]
+[RhoMicro.CodeAnalysis.GenerateFactory]
+#endif
+[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
+internal sealed partial class ValueEqualityAttribute : Attribute;
diff --git a/Janus.Analyzers/AnalyzerReleases.Shipped.md b/Janus.Analyzers/AnalyzerReleases.Shipped.md
new file mode 100644
index 0000000..f3c849d
--- /dev/null
+++ b/Janus.Analyzers/AnalyzerReleases.Shipped.md
@@ -0,0 +1,25 @@
+## Release 23.0.0
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|--------------------
+ RMJ0001 | Usage | Warning | `ToStringSetting` is ignored due to user defined `ToString` implementation
+ RMJ0002 | Usage | Error | `record` unions are disallowed
+ RMJ0003 | Usage | Error | Generic unions cannot be json serializable
+ RMJ0004 | Usage | Error | No more than 31 variant groups may be defined
+ RMJ0005 | Usage | Error | `static` unions are disallowed
+ RMJ0006 | Usage | Error | Duplicate variant names are disallowed
+ RMJ0007 | Design | Warning | At least one unmanaged or managed struct or nullable reference type variant must be defined for struct unions
+ RMJ0008 | Design | Warning | `interface` variants are excluded from conversion operators
+ RMJ0009 | Usage | Error | Duplicate variant types are disallowed
+ RMJ0010 | Usage | Error | `object` cannot be used as a variant
+ RMJ0012 | Usage | Error | Variants that are the union itself are disallowed
+ RMJ0013 | Usage | Error | Explicitly defined base classes are disallowed
+ RMJ0014 | Usage | Warning | Prefer `Nullable` over `IsNullable = true`
+ RMJ0015 | Usage | Error | `Nullable` and `T` variants for the same type `T` are disallowed
+ RMJ0016 | Usage | Warning | `UnionTypeSettings` are ignored if missing `UnionTypeAttribute`
+ RMJ0017 | Usage | Warning | Duplicate variant group names are ignored
+ RMJ0018 | Design | Warning | Class unions should be sealed
+ RMJ0019 | Usage | Error | `ValueType` cannot be used as a variant of struct unions
+ RMJ0020 | Usage | Error | `ref struct` cannot be union
diff --git a/Janus.Analyzers/AnalyzerReleases.Unshipped.md b/Janus.Analyzers/AnalyzerReleases.Unshipped.md
new file mode 100644
index 0000000..e69de29
diff --git a/Janus.Analyzers/Components/ComponentFactory.cs b/Janus.Analyzers/Components/ComponentFactory.cs
new file mode 100644
index 0000000..98e892a
--- /dev/null
+++ b/Janus.Analyzers/Components/ComponentFactory.cs
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Lyra;
+
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using Janus;
+using Library.Models.Collections;
+
+partial class ComponentFactory
+{
+ partial class Docs
+ {
+ public static DocsCommentElementComponent SwitchReturns() =>
+ Returns("The result produced by the invoked handler.");
+
+ public static DocsCommentElementComponent SwitchSummary() =>
+ Summary("Invokes a handler based on the represented variant.");
+
+ public static DocsCommentElementComponent<(String, String)> SwitchStateParam() =>
+ Param("state", "The state to pass to handlers.");
+
+ public static DocsCommentElementComponent<(String, String)> SwitchStateTypeparam() =>
+ TypeParam("TState", "The type of state to pass to handlers.");
+
+ public static DocsCommentElementComponent<(String, String)> SwitchResultTypeparam() =>
+ TypeParam("TResult", "The type of result returned by handlers.");
+
+ public static DocsCommentElementComponent<(String, String)> SwitchDefaultHandlerParam() =>
+ Param("defaultHandler", "The handler to invoke if no handler for the represented variant was passed.");
+
+ public static DocsCommentElementComponent<(String, String)> SwitchDefaultResultParam() =>
+ Param("defaultResult", "The result to return if no handler for the represented variant was passed.");
+
+ public static StrategyComponent SwitchHandlerParams(UnionModel model) =>
+ Create(model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(ComponentFactory.List(m.Variants, static (v, i, l, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(SwitchHandlerParam(v));
+ }, separator: "\n"));
+ });
+
+ public static
+ DocsCommentElementComponent<(
+ String,
+ UnionTypeAttribute.Model,
+ Action)>
+ SwitchHandlerParam(UnionTypeAttribute.Model variant) =>
+ Param($"on{variant.Name}", variant, static (v, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The handler to invoke if the union is of the {C(v.Name)} variant.");
+ });
+ }
+
+ public static ListComponent, TElement, String> List(
+ EquatableList list,
+ Action append,
+ String separator = "",
+ String terminator = "")
+ => List, TElement>(
+ list,
+ append,
+ separator,
+ terminator);
+
+ public static ListComponent, String, String> List(
+ EquatableList list,
+ Action append,
+ String separator = "",
+ String terminator = "")
+ => List, String>(
+ list,
+ append,
+ separator,
+ terminator);
+
+ public static ListComponent, UnionTypeAttribute.Model, String> List(
+ EquatableList list,
+ Action append,
+ String separator = "",
+ String terminator = "")
+ => List, UnionTypeAttribute.Model>(
+ list,
+ append,
+ separator,
+ terminator);
+
+ public static ListComponent List(
+ String[] list,
+ Action append,
+ String separator = "",
+ String terminator = "")
+ => List(
+ list,
+ append,
+ separator,
+ terminator);
+}
diff --git a/Janus.Analyzers/Components/ConstructorComponent.cs b/Janus.Analyzers/Components/ConstructorComponent.cs
new file mode 100644
index 0000000..70c5475
--- /dev/null
+++ b/Janus.Analyzers/Components/ConstructorComponent.cs
@@ -0,0 +1,133 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using System.Runtime.CompilerServices;
+using Lyra;
+using static Lyra.ComponentFactory;
+using static Lyra.ComponentFactory.Docs;
+
+internal readonly record struct ConstructorComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var body = Create(this, static (@this, b, _) =>
+ {
+ var model = @this.Model;
+
+ foreach (var variant in model.Variants)
+ {
+ b.AppendLine(
+ $$"""
+ {{Summary("Initializes a new instance.")}}
+ {{Param("value", "The variant value to initialize the new instance with.")}}
+ [{{typeof(OverloadResolutionPriorityAttribute)}}(1)]
+ public {{model.Name}}({{variant.Type.NullableName}} value) : this(value, validate: true) { }
+
+ {{Summary("Initializes a new instance.")}}
+ {{Param("value", "The variant value to initialize the new instance with.")}}
+ {{Param("validate", "Indicates whether to validate the value.")}}
+ private {{model.Name}}({{variant.Type.NullableName}} value, bool validate)
+ {
+ if (validate)
+ {
+ var isValid = true;
+ Validate(value, throwIfInvalid: true, ref isValid);
+ }
+
+ {{Create(variant, static (v, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ switch (v.Type.Kind)
+ {
+ case VariantTypeKind.Unmanaged:
+ b.Append($"this._unmanagedVariantsContainer = new UnmanagedVariantsContainer(value);");
+ break;
+ case VariantTypeKind.Value or VariantTypeKind.Unknown:
+ b.Append($"this._{v.Name} = value;");
+ break;
+ case VariantTypeKind.Reference:
+ b.Append("this._referenceVariantsContainer = value;");
+ break;
+ }
+ })}}
+ this.Variant = VariantModel.{{variant.Name}};
+ }
+ """
+ );
+ }
+
+ b.Append(
+ $$"""
+ {{Summary("Initializes a new instance.")}}
+ {{Param("value", "The instance whose variant value to use when initializing the new instance.")}}
+ [{{typeof(OverloadResolutionPriorityAttribute)}}(0)]
+ public {{model.Name}}({{new UnionTypeNameComponent(model)}} value)
+ {
+ {{Create(model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var unmanagedAssigned = false;
+ var referenceAssigned = false;
+
+ for (var i = 0; i < m.Variants.Count; i++)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var variant = m.Variants[i];
+ switch (variant.Type.Kind)
+ {
+ case VariantTypeKind.Unmanaged:
+ if (unmanagedAssigned)
+ {
+ continue;
+ }
+
+ if (i is not 0)
+ {
+ b.AppendLine();
+ }
+
+ b.Append("this._unmanagedVariantsContainer = value._unmanagedVariantsContainer;");
+ unmanagedAssigned = true;
+ break;
+ case VariantTypeKind.Reference:
+ if (referenceAssigned)
+ {
+ continue;
+ }
+
+ if (i is not 0)
+ {
+ b.AppendLine();
+ }
+
+ referenceAssigned = true;
+ b.Append("this._referenceVariantsContainer = value._referenceVariantsContainer;");
+ break;
+ case VariantTypeKind.Value or VariantTypeKind.Unknown:
+ if (i is not 0)
+ {
+ b.AppendLine();
+ }
+
+ b.Append($"this._{variant.Name} = value._{variant.Name};");
+ break;
+ }
+ }
+ })}}
+ this.Variant = value.Variant;
+ }
+ """
+ );
+ });
+
+ var region = Region("Constructors", body);
+
+ builder.Append(region);
+ }
+}
diff --git a/Janus.Analyzers/Components/EqualityComponent.cs b/Janus.Analyzers/Components/EqualityComponent.cs
new file mode 100644
index 0000000..03bb174
--- /dev/null
+++ b/Janus.Analyzers/Components/EqualityComponent.cs
@@ -0,0 +1,106 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using Lyra;
+using static Lyra.ComponentFactory;
+using static Lyra.ComponentFactory.Docs;
+
+internal readonly record struct EqualityComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var methods = ComponentFactory.Create(Model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$"""
+ {{Inheritdoc()}}
+ public override bool Equals(object? obj) => obj is {{new UnionTypeNameComponent(m)}} other && Equals(other);
+ """
+ );
+
+ if (!m.IsEqualsUserProvided)
+ {
+ b.Append(
+ $$"""
+
+ {{Inheritdoc()}}
+ public bool Equals({{new UnionTypeNameComponent(m, RenderNullable: true)}} other)
+ {
+ {{ComponentFactory.Create(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (m.TypeKind is not UnionTypeKind.Class)
+ {
+ return;
+ }
+
+ b.Append(
+ $$"""
+ if(other is null)
+ {
+ return false;
+ }
+
+ if(object.ReferenceEquals(this, other))
+ {
+ return true;
+ }
+
+
+ """
+ );
+ })}}if(Variant != other.Variant)
+ {
+ return false;
+ }
+
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"return global::System.Collections.Generic.EqualityComparer<{v.Type.NullableName}>.Default.Equals({new VariantAccessorComponent(v)}, {new VariantAccessorComponent(v, InstanceExpression: "other")});");
+ })}}
+ }
+ """
+ );
+ }
+
+ if (!m.IsGetHashCodeUserProvided)
+ {
+ b.Append($$"""
+
+ {{Inheritdoc()}}
+ public override int GetHashCode()
+ {
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"return {typeof(HashCode)}.Combine(Variant, {new VariantAccessorComponent(v)});");
+ })}}
+ }
+ """
+ );
+ }
+
+ b.Append(
+ $"""
+
+ {new EqualityOperatorComponent(m)}
+
+ {new ToStringComponent(m)}
+ """
+ );
+ });
+
+ var region = Region("Equality & ToString", methods);
+
+ builder.Append(region);
+ }
+}
diff --git a/Janus.Analyzers/Components/EqualityOperatorComponent.cs b/Janus.Analyzers/Components/EqualityOperatorComponent.cs
new file mode 100644
index 0000000..64460b5
--- /dev/null
+++ b/Janus.Analyzers/Components/EqualityOperatorComponent.cs
@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using Lyra;
+using static Lyra.ComponentFactory;
+using static Lyra.ComponentFactory.Docs;
+
+internal readonly record struct EqualityOperatorComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (Model is
+ {
+ AreEqualityOperatorsUserProvided: true
+ } or
+ {
+ Settings.EqualityOperatorsSetting: EqualityOperatorsSetting.OmitOperators
+ }
+ or
+ {
+ Settings.EqualityOperatorsSetting: EqualityOperatorsSetting.EmitOperatorsIfValueType,
+ TypeKind:
+ not UnionTypeKind.Struct
+ })
+ {
+ return;
+ }
+
+ builder.Append($$"""
+ {{Inheritdoc()}}
+ public static bool operator ==({{new UnionTypeNameComponent(Model)}} x, {{new UnionTypeNameComponent(Model)}} y) =>
+ global::System.Collections.Generic.EqualityComparer<{{new UnionTypeNameComponent(Model)}}>.Default.Equals(x, y);
+ {{Inheritdoc()}}
+ public static bool operator !=({{new UnionTypeNameComponent(Model)}} x, {{new UnionTypeNameComponent(Model)}} y) =>
+ !(x == y);
+ """
+ );
+ }
+}
diff --git a/Janus.Analyzers/Components/FactoriesComponent.cs b/Janus.Analyzers/Components/FactoriesComponent.cs
new file mode 100644
index 0000000..2ee34ac
--- /dev/null
+++ b/Janus.Analyzers/Components/FactoriesComponent.cs
@@ -0,0 +1,250 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using System.Diagnostics.CodeAnalysis;
+using Lyra;
+using static Lyra.ComponentFactory;
+using static Lyra.ComponentFactory.Docs;
+
+internal readonly record struct FactoriesComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var methods = Create(Model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.AppendLine(
+ $$"""
+ {{Summary(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Creates an instance of {Cref(m.DocsCommentId)}.");
+ })}}
+ {{Param("value", m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The value to create an instance of {Cref(m.DocsCommentId)} from.");
+ })}}
+ {{TypeParam("TVariant", m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The type of the value to create an instance of {Cref(m.DocsCommentId)} from.");
+ })}}
+ {{Remarks(static (b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"If more than one variant of the union implement {TypeParamRef("TVariant")}, the selected variant is not specified and may change in future versions.");
+ })}}
+ {{Returns(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The new instance of {Cref(m.DocsCommentId)}.");
+ })}}
+ public static {{new UnionTypeNameComponent(m)}} Create(
+ TVariant value)
+ => new Factory().Create(value);
+
+ {{Summary(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Attempts to create an instance of {Cref(m.DocsCommentId)}.");
+ })}}
+ {{Param("value", m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The value to create an instance of {Cref(m.DocsCommentId)} from.");
+ })}}
+ {{Param("union", m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $"""
+ The instance of {Cref(m.DocsCommentId)} if one could be created; otherwise,
+ {(m.TypeKind is UnionTypeKind.Class ? Langword("null") : Langword("default"))}.
+ """);
+ })}}
+ {{TypeParam("TVariant", m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The type of the value to create an instance of {Cref(m.DocsCommentId)} from.");
+ })}}
+ {{Remarks(static (b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"If more than one variant of the union implement {TypeParamRef("TVariant")}, the selected variant is not specified and may change in future versions.");
+ })}}
+ {{Returns(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"{Langword("true")} if an instance of {Cref(m.DocsCommentId)} could be created; otherwise, {Langword("false")}.");
+ })}}
+ public static bool TryCreate(
+ TVariant value,
+ [{{typeof(NotNullWhenAttribute)}}(true)]
+ out {{new UnionTypeNameComponent(m, RenderNullable: true)}} union)
+ => new Factory().TryCreate(value, out union);
+
+ {{Summary(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Creates an instance of {Cref(m.DocsCommentId)}.");
+ })}}
+ {{Param("value", m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The value to create an instance of {Cref(m.DocsCommentId)} from.");
+ })}}
+ {{Returns(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The new instance of {Cref(m.DocsCommentId)}.");
+ })}}
+ public static {{new UnionTypeNameComponent(m)}} Create(
+ {{new UnionTypeNameComponent(m)}} value)
+ => new(value);
+
+ {{Summary(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Attempts to create an instance of {Cref(m.DocsCommentId)}.");
+ })}}
+ {{Param("value", m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The value to create an instance of {Cref(m.DocsCommentId)} from.");
+ })}}
+ {{Param("union", m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $"""
+ The instance of {Cref(m.DocsCommentId)} if one could be created; otherwise,
+ {(m.TypeKind is UnionTypeKind.Class ? Langword("null") : Langword("default"))}.
+ """);
+ })}}
+ {{Returns(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"{Langword("true")} if an instance of {Cref(m.DocsCommentId)} could be created; otherwise, {Langword("false")}.");
+ })}}
+ public static bool TryCreate(
+ {{new UnionTypeNameComponent(m)}} value,
+ [{{typeof(NotNullWhenAttribute)}}(true)]
+ out {{new UnionTypeNameComponent(m, RenderNullable: true)}} union)
+ {
+ union = Create(value);
+
+ return true;
+ }
+
+ """
+ );
+
+ for (var i = 0; i < m.Variants.Count; i++)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (i is not 0)
+ {
+ b.AppendLine().AppendLine();
+ }
+
+ var variant = m.Variants[i];
+
+ b.Append(
+ $$"""
+ {{Summary(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Creates an instance of {Cref(m.DocsCommentId)}.");
+ })}}
+ {{Param("value", m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The value to create an instance of {Cref(m.DocsCommentId)} from.");
+ })}}
+ {{Returns(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The new instance of {Cref(m.DocsCommentId)}.");
+ })}}
+ public static {{new UnionTypeNameComponent(m)}} Create(
+ {{variant.Type.NullableName}} value)
+ => new {{new UnionTypeNameComponent(m)}}(value, validate: true);
+
+ {{Summary(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Attempts to create an instance of {Cref(m.DocsCommentId)}.");
+ })}}
+ {{Param("value", m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The value to create an instance of {Cref(m.DocsCommentId)} from.");
+ })}}
+ {{Param("union", m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $"""
+ The instance of {Cref(m.DocsCommentId)} if one could be created; otherwise,
+ {(m.TypeKind is UnionTypeKind.Class ? Langword("null") : Langword("default"))}.
+ """);
+ })}}
+ {{Returns(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"{Langword("true")} if an instance of {Cref(m.DocsCommentId)} could be created; otherwise, {Langword("false")}.");
+ })}}
+ public static bool TryCreate(
+ {{variant.Type.NullableName}} value,
+ [{{typeof(NotNullWhenAttribute)}}(true)]
+ out {{new UnionTypeNameComponent(m, RenderNullable: true)}} union)
+ {
+ var isValid = true;
+ Validate(value, throwIfInvalid: false, ref isValid);
+ union = isValid
+ ? new {{new UnionTypeNameComponent(m)}}(value, validate: false)
+ : default;
+
+ return isValid;
+ }
+ """
+ );
+ }
+ });
+
+ var region = Region("Factories", methods);
+
+ builder.Append(region);
+ }
+}
diff --git a/Janus.Analyzers/Components/FactoryComponent.cs b/Janus.Analyzers/Components/FactoryComponent.cs
new file mode 100644
index 0000000..3dbbdcb
--- /dev/null
+++ b/Janus.Analyzers/Components/FactoryComponent.cs
@@ -0,0 +1,184 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using System.Diagnostics.CodeAnalysis;
+using Lyra;
+using static Lyra.ComponentFactory;
+using static Lyra.ComponentFactory.Docs;
+
+internal readonly record struct FactoryComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var members = Create(
+ Model,
+ static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.AppendLine(
+ $$"""
+ {{Summary(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Attempts to create an instance of {Cref(m.DocsCommentId)}.");
+ })}}
+ {{Param("value", m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The value to create an instance of {Cref(m.DocsCommentId)} from.");
+ })}}
+ {{Param("union", m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $"""
+ The instance of {Cref(m.DocsCommentId)} if one could be created; otherwise,
+ {(m.TypeKind is UnionTypeKind.Class ? Langword("null") : Langword("default"))}.
+ """);
+ })}}
+ {{TypeParam("TVariant", m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The type of the value to create an instance of {Cref(m.DocsCommentId)} from.");
+ })}}
+ {{Remarks(static (b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"If more than one variant of the union implement {TypeParamRef("TVariant")}, the selected variant is not specified and may change in future versions.");
+ })}}
+ {{Returns(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"{Langword("true")} if an instance of {Cref(m.DocsCommentId)} could be created; otherwise, {Langword("false")}.");
+ })}}
+ public {{typeof(Boolean)}} TryCreate(
+ TVariant value,
+ [{{TypeName()}}(true)]
+ out {{new UnionTypeNameComponent(m, RenderNullable: true)}} union)
+ {
+ switch (value)
+ {
+ {{Create(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ for (var i = 0; i < m.Variants.Count; i++)
+ {
+ ct.ThrowIfCancellationRequested();
+ var variant = m.Variants[i];
+
+ if (i is not 0)
+ {
+ b.AppendLine();
+ }
+
+ b.Append($"case {variant.Type.Name} v: return {new UnionTypeNameComponent(m)}.TryCreate(v, out union);");
+ }
+ })}}
+ case {{new UnionTypeNameComponent(m)}} v:
+ union = new {{new UnionTypeNameComponent(m)}}(v);
+ return true;
+ case {{m.TypeNames.IUnion}} v: return v.TryMapTo(this, out union);
+ default:
+ union = default;
+ return false;
+ }
+ }
+
+ {{Summary(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Creates an instance of {Cref(m.DocsCommentId)}.");
+ })}}
+ {{Param("value", m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The value to create an instance of {Cref(m.DocsCommentId)} from.");
+ })}}
+ {{TypeParam("TVariant", m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The type of the value to create an instance of {Cref(m.DocsCommentId)} from.");
+ })}}
+ {{Remarks(static (b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"If more than one variant of the union implement {TypeParamRef("TVariant")}, the selected variant is not specified and may change in future versions.");
+ })}}
+ {{Returns(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The new instance of {Cref(m.DocsCommentId)}.");
+ })}}
+ public {{new UnionTypeNameComponent(m)}} Create(TVariant value)
+ {
+ switch(value)
+ {
+ {{Create(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ for (var i = 0; i < m.Variants.Count; i++)
+ {
+ ct.ThrowIfCancellationRequested();
+ var variant = m.Variants[i];
+
+ if (i is not 0)
+ {
+ b.AppendLine();
+ }
+
+ b.Append($"case {variant.Type.Name} v: return new {new UnionTypeNameComponent(m)}(v);");
+ }
+ })}}
+ case {{new UnionTypeNameComponent(m)}} v: return new {{new UnionTypeNameComponent(m)}}(v);
+ case {{m.TypeNames.IUnion}} v: return v.MapTo<{{new UnionTypeNameComponent(m)}}, Factory>(this);
+ default:
+ throw new {{typeof(ArgumentOutOfRangeException)}}(nameof(value), value, $"Unable to create an instance of '{typeof({{new UnionTypeNameComponent(m)}})}' from a value of type '{value?.GetType() ?? typeof(TVariant)}': {(value is null ? "null" : $"'{value}'")}");
+ }
+ }
+ """
+ );
+ });
+
+ var type = Create((Model, members), static (t, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var (model, members) = t;
+
+ b.Append($"""
+ {Summary(model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Creates instances of {Cref(m.DocsCommentId)}.");
+ })}
+ {Type(
+ "private readonly struct",
+ "Factory",
+ members,
+ baseTypeList: [model.TypeNames.IUnionFactory])}
+ """);
+ });
+
+ var region = Region("Factory", type);
+
+ builder.Append(region);
+ }
+}
diff --git a/Janus.Analyzers/Components/FieldsComponent.cs b/Janus.Analyzers/Components/FieldsComponent.cs
new file mode 100644
index 0000000..3621c0d
--- /dev/null
+++ b/Janus.Analyzers/Components/FieldsComponent.cs
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using Lyra;
+using static Lyra.ComponentFactory;
+using static Lyra.ComponentFactory.Docs;
+
+internal readonly record struct FieldsComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var fields = Create(Model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var unmanagedDefined = false;
+ var referenceDefined = false;
+
+ for (var i = 0; i < m.Variants.Count; i++)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var variant = m.Variants[i];
+
+ switch (variant.Type.Kind)
+ {
+ case VariantTypeKind.Unmanaged:
+ if (unmanagedDefined)
+ {
+ continue;
+ }
+
+ if (i is not 0)
+ {
+ b.AppendLine();
+ }
+
+ b.Append(
+ $"""
+ {Summary("The container used to store values for unmanaged variants of the union.")}
+ private readonly UnmanagedVariantsContainer _unmanagedVariantsContainer = default;
+ """);
+ unmanagedDefined = true;
+ break;
+ case VariantTypeKind.Value or VariantTypeKind.Unknown:
+ if (i is not 0)
+ {
+ b.AppendLine();
+ }
+
+ b.Append($"""
+ {Summary(variant, static (v, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The value when representing the {C(v.Name)} variant.");
+ })}
+ private readonly {variant.Type.NullableName} _{variant.Name} = default;
+ """);
+ break;
+ case VariantTypeKind.Reference:
+ if (referenceDefined)
+ {
+ continue;
+ }
+
+ if (i is not 0)
+ {
+ b.AppendLine();
+ }
+
+ b.Append($"""
+ {Summary("The container used to store values for reference type variants.")}
+ private readonly object? _referenceVariantsContainer = default;
+ """);
+ referenceDefined = true;
+ break;
+ }
+ }
+ });
+
+ var region = Region("Fields", fields);
+
+ builder.Append(region);
+ }
+}
diff --git a/Janus.Analyzers/Components/InspectionsComponent.cs b/Janus.Analyzers/Components/InspectionsComponent.cs
new file mode 100644
index 0000000..b3fdecb
--- /dev/null
+++ b/Janus.Analyzers/Components/InspectionsComponent.cs
@@ -0,0 +1,232 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using Lyra;
+using static Lyra.ComponentFactory;
+using static Lyra.ComponentFactory.Docs;
+
+internal readonly record struct InspectionsComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var methods = Create(Model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var mayBeNull = m.Variants.Any(static v =>
+ v.Type is { IsNullable: true } or { Kind: VariantTypeKind.Unknown });
+
+ b.Append(
+ $$"""
+ {{Summary("""
+ Converts the value contained by the union to the provided type. If the variant of the union
+ is convertible to the provided type, a conversion will be performed; otherwise, an exception
+ is thrown.
+ """)}}
+ {{TypeParam("TVariant", "The type to convert the unions value to.")}}
+ {{Returns("The converted value.")}}
+ public TVariant{{(mayBeNull ? "?" : String.Empty)}} CastTo()
+ {
+ {{new VariantsSwitchComponent(m, static (v, m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var annotation = v.Type is { IsNullable: true } or { Kind: VariantTypeKind.Unknown } ? "?" : String.Empty;
+
+ b.Append(
+ $"""
+ // The jit will optimize this conversion and prevent boxing.
+ var result = (TVariant{annotation})(object{annotation}){new VariantAccessorComponent(v)};
+
+ return result;
+ """
+ );
+ })}}
+ }
+
+ {{Summary("Attempts to convert the value contained by the union to the provided type.")}}
+ {{TypeParam("TVariant", "The type to convert the unions value to.")}}
+ {{Returns(static (b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The converted value if the conversion is possible; otherwise, {Langword("default")}.");
+ })}}
+ public TVariant? As()
+ {
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var annotation = v.Type is { IsNullable: true } or { Kind: VariantTypeKind.Unknown } ? "?" : String.Empty;
+
+ b.Append(
+ $$"""
+ if(typeof(TVariant) == typeof({{v.Type.NullableValueTypeName}}) || typeof(TVariant).IsAssignableFrom(typeof({{v.Type.NullableValueTypeName}})))
+ {
+ // The jit will optimize this conversion and prevent boxing.
+ var result = (TVariant{{annotation}})(object{{annotation}}){{new VariantAccessorComponent(v)}};
+
+ return result;
+ }
+ else
+ {
+ return default;
+ }
+ """
+ );
+ })}}
+ }
+
+ {{Summary("Attempts to convert the value contained by the union to the provided type.")}}
+ {{Param("value", static (b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The converted value if the conversion is possible; otherwise, {Langword("default")}.");
+ })}}
+ {{TypeParam("TVariant", "The type to convert the unions value to.")}}{{Create(mayBeNull, static (f, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (!f)
+ {
+ return;
+ }
+
+ b.Append($"""
+
+ {Remarks(static (b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Because this union may have a nullable value, {ParamRef("value")} is not guaranteed to be a non-{Langword("null")} value upon returning {Langword("true")}.");
+ })}
+ """);
+ })}}
+ {{Returns(static (b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"{Langword("true")} if the conversion is possible; otherwise, {Langword("false")}.");
+ })}}
+ public bool TryCastTo({{Create(mayBeNull, static (f, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (f)
+ {
+ return;
+ }
+
+ b.Append($"[{typeof(NotNullWhenAttribute)}(true)] ");
+ })}}out TVariant? value)
+ {
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var annotation = v.Type is { IsNullable: true } or { Kind: VariantTypeKind.Unknown } ? "?" : String.Empty;
+
+ b.Append(
+ $$"""
+ if(typeof(TVariant) == typeof({{v.Type.NullableValueTypeName}}) || typeof(TVariant).IsAssignableFrom(typeof({{v.Type.NullableValueTypeName}})))
+ {
+ // The jit will optimize this conversion and prevent boxing.
+ value = (TVariant{{annotation}})(object{{annotation}}){{new VariantAccessorComponent(v)}};
+
+ return true;
+ }
+ else
+ {
+ value = default;
+ return false;
+ }
+ """
+ );
+ })}}
+ }
+
+ {{Summary("Gets a value indicating whether the unions variant is convertible to the specified type.")}}
+ {{TypeParam("TVariant", "The type to check against the variant of the union.")}}
+ {{Returns(static (b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"{Langword("true")} if the variant of the union is convertible to {TypeParamRef("TVariant")}; otherwise, {Langword("false")}.");
+ })}}
+ public bool Is()
+ {
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"""
+ var result = typeof(TVariant) == typeof({v.Type.NullableValueTypeName}) || typeof(TVariant).IsAssignableFrom(typeof({v.Type.NullableValueTypeName}));
+
+ return result;
+ """);
+ })}}
+ }
+
+ {{List(m.Variants, static (v, i, l, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$"""
+ {{Summary(v, static (v, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Attempt to get the represented value if the union is of the {C(v.Name)} variant.");
+ })}}
+ {{Param("value", v, static (v, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The unions value if the union is of the {C(v.Name)} variant; otherwise, {(v.Type.Kind is VariantTypeKind.Reference ? Langword("null") : Langword("default"))}.");
+ })}}
+ {{Remarks(v, static (v, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"""
+ If the union is not of the {C(v.Name)} variant, no conversion is attempted.
+ In order to cast to a specific type, use {Cref("CastTo{TVariant}")} instead.
+ """);
+ })}}
+ {{Returns(v, static (v, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"{Langword("true")} if the union is of the {C(v.Name)}; otherwise, {Langword("false")}.");
+ })}}
+ public bool TryCastTo{{v.Name}}({{(v.Type is { IsNullable: false, Kind: VariantTypeKind.Reference } ? "[global::System.Diagnostics.CodeAnalysis.NotNullWhen(true)]" : String.Empty)}}out {{v.Type.NullableName}}{{(v.Type is { IsNullable: false, Kind: VariantTypeKind.Reference } ? "?" : String.Empty)}} value)
+ {
+ if(Is{{v.Name}})
+ {
+ value = CastTo{{v.Name}};
+ return true;
+ }
+
+ value = default;
+ return false;
+ }
+ """
+ );
+ }, "\n")}}
+ """
+ );
+ });
+
+ var region = Region("Inspection", methods);
+
+ builder.Append(region);
+ }
+}
diff --git a/Janus.Analyzers/Components/JsonConverterComponent.cs b/Janus.Analyzers/Components/JsonConverterComponent.cs
new file mode 100644
index 0000000..e6412f5
--- /dev/null
+++ b/Janus.Analyzers/Components/JsonConverterComponent.cs
@@ -0,0 +1,273 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using Lyra;
+using static Lyra.ComponentFactory;
+using static Lyra.ComponentFactory.Docs;
+
+internal readonly record struct JsonConverterComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var members = Create(Model, static (m, b, _) =>
+ {
+ b.Append(
+ $$"""
+ private static readonly global::System.Collections.Generic.HashSet _knownNamingPolicies =
+ [
+ global::System.Text.Json.JsonNamingPolicy.CamelCase,
+ global::System.Text.Json.JsonNamingPolicy.KebabCaseLower,
+ global::System.Text.Json.JsonNamingPolicy.KebabCaseUpper,
+ global::System.Text.Json.JsonNamingPolicy.SnakeCaseLower,
+ global::System.Text.Json.JsonNamingPolicy.SnakeCaseUpper
+ ];
+
+ private static readonly byte[] _variantDefaultCase = global::System.Text.Encoding.UTF8.GetBytes("Variant");
+ private static readonly byte[] _variantLowerCase = global::System.Text.Encoding.UTF8.GetBytes("variant");
+ private static readonly byte[] _valueDefaultCase = global::System.Text.Encoding.UTF8.GetBytes("Value");
+ private static readonly byte[] _valueLowerCase = global::System.Text.Encoding.UTF8.GetBytes("value");
+
+ {{Inheritdoc()}}
+ public override {{new UnionTypeNameComponent(m, RenderNullable: true)}} Read(
+ ref global::System.Text.Json.Utf8JsonReader reader,
+ global::System.Type typeToConvert,
+ global::System.Text.Json.JsonSerializerOptions options)
+ {
+ if (!global::System.Text.Json.JsonElement.TryParseValue(ref reader, out var e) ||
+ e is not { ValueKind: global::System.Text.Json.JsonValueKind.Object } unionObject)
+ {
+ throw new global::System.Text.Json.JsonException("Unable to read a union object value from the reader.");
+ }
+
+ if (!TryReadVariantPropertyValue(unionObject, options, out var variantElement))
+ {
+ throw new global::System.Text.Json.JsonException(
+ $"Unable to read a value for the '{nameof(Variant)}' property of the union object.");
+ }
+
+ var variant = global::System.Text.Json.JsonSerializer.Deserialize(variantElement, options);
+
+ if (!TryReadValuePropertyValue(unionObject, options, out var valueElement))
+ {
+ throw new global::System.Text.Json.JsonException(
+ $"Unable to read a value for the '{nameof(Value)}' property of the union object.");
+ }
+
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"return global::System.Text.Json.JsonSerializer.Deserialize<{v.Type.NullableName}>(valueElement, options)");
+
+ if (v.Type is { IsNullable: false, Kind: VariantTypeKind.Reference })
+ {
+ b.Append($$"""
+ ?? throw new global::System.Text.Json.JsonException($"Unable to deserialize a non-null value for the represented non-nullable variant '{nameof(VariantKind.{{v.Name}})}' of type '{typeof({{v.Type.NullableName}})}'.")
+ """
+ );
+ }
+
+ b.Append(';');
+ }, static (_, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ """
+ var exception = new global::System.Text.Json.JsonException(
+ $"Unable to read a value for the '{nameof(Value)}' property of the union object, as the " +
+ $"'{nameof(Variant)}' property is not representing a valid variant of this union: " +
+ $"'{variant}'. This could be either because the union itself was not serialized " +
+ "correctly, or due to a bug in the 'Janus' Janussource generator that generated this union " +
+ "type. Please verify that the serialized data is in the correct format, and, if " +
+ "necessary, report an issue to the maintainer. The json used for deserialization " +
+ "has been attached to this exception.");
+
+ exception.Data[$"{GetType()}.Data"] =
+ global::System.Text.Json.JsonSerializer.Serialize(unionObject, options);
+
+ throw exception;
+ """
+ );
+ }, "variant")}}
+ }
+
+ private bool TryReadVariantPropertyValue(
+ global::System.Text.Json.JsonElement unionObject,
+ global::System.Text.Json.JsonSerializerOptions options,
+ out global::System.Text.Json.JsonElement value)
+ => TryReadPropertyValue(
+ unionObject,
+ options,
+ defaultName: nameof(Variant),
+ defaultUtf8Name: _variantDefaultCase,
+ lowerCaseUtf8Name: _variantLowerCase,
+ out value);
+
+ private bool TryReadValuePropertyValue(
+ global::System.Text.Json.JsonElement unionObject,
+ global::System.Text.Json.JsonSerializerOptions options,
+ out global::System.Text.Json.JsonElement value)
+ => TryReadPropertyValue(
+ unionObject,
+ options,
+ defaultName: nameof(Value),
+ defaultUtf8Name: _valueDefaultCase,
+ lowerCaseUtf8Name: _valueLowerCase,
+ out value);
+
+ private bool TryReadPropertyValue(
+ global::System.Text.Json.JsonElement unionObject,
+ global::System.Text.Json.JsonSerializerOptions options,
+ string defaultName,
+ global::System.ReadOnlySpan defaultUtf8Name,
+ global::System.ReadOnlySpan lowerCaseUtf8Name,
+ out global::System.Text.Json.JsonElement value)
+ {
+ if (options.PropertyNamingPolicy == null || // Pascal case (for our properties)
+ options.PropertyNamingPolicy == global::System.Text.Json.JsonNamingPolicy.SnakeCaseUpper ||
+ options.PropertyNamingPolicy == global::System.Text.Json.JsonNamingPolicy.KebabCaseUpper)
+ {
+ return unionObject.TryGetProperty(defaultUtf8Name, out value) ||
+ options.PropertyNameCaseInsensitive &&
+ unionObject.TryGetProperty(lowerCaseUtf8Name, out value);
+ }
+
+ if (options.PropertyNamingPolicy == global::System.Text.Json.JsonNamingPolicy.CamelCase ||
+ options.PropertyNamingPolicy == global::System.Text.Json.JsonNamingPolicy.SnakeCaseLower ||
+ options.PropertyNamingPolicy == global::System.Text.Json.JsonNamingPolicy.KebabCaseLower)
+ {
+ return unionObject.TryGetProperty(lowerCaseUtf8Name, out value) ||
+ options.PropertyNameCaseInsensitive &&
+ unionObject.TryGetProperty(defaultUtf8Name, out value);
+ }
+
+ var convertedName = options.PropertyNamingPolicy.ConvertName(defaultName);
+ var comparison = options.PropertyNameCaseInsensitive
+ ? global::System.StringComparison.OrdinalIgnoreCase
+ : global::System.StringComparison.Ordinal;
+ foreach (var property in unionObject.EnumerateObject())
+ {
+ if (!property.Name.Equals(convertedName, comparison))
+ {
+ continue;
+ }
+
+ value = property.Value;
+ return true;
+ }
+
+ value = default;
+ return false;
+ }
+
+ private void WriteVariantPropertyName(global::System.Text.Json.Utf8JsonWriter writer, global::System.Text.Json.JsonSerializerOptions options)
+ => WritePropertyName(
+ writer,
+ options,
+ defaultName: nameof(Variant),
+ defaultUtf8Name: _variantDefaultCase,
+ lowerCaseUtf8Name: _variantLowerCase);
+
+ private void WriteValuePropertyName(global::System.Text.Json.Utf8JsonWriter writer, global::System.Text.Json.JsonSerializerOptions options)
+ => WritePropertyName(
+ writer,
+ options,
+ defaultName: nameof(Value),
+ defaultUtf8Name: _valueDefaultCase,
+ lowerCaseUtf8Name: _valueLowerCase);
+
+ private void WritePropertyName(
+ global::System.Text.Json.Utf8JsonWriter writer,
+ global::System.Text.Json.JsonSerializerOptions options,
+ string defaultName,
+ global::System.ReadOnlySpan defaultUtf8Name,
+ global::System.ReadOnlySpan lowerCaseUtf8Name)
+ {
+ if (options.PropertyNamingPolicy == null ||
+ options.PropertyNamingPolicy == global::System.Text.Json.JsonNamingPolicy.SnakeCaseUpper ||
+ options.PropertyNamingPolicy == global::System.Text.Json.JsonNamingPolicy.KebabCaseUpper)
+ {
+ writer.WritePropertyName(defaultUtf8Name);
+ return;
+ }
+
+ if (options.PropertyNamingPolicy == global::System.Text.Json.JsonNamingPolicy.CamelCase ||
+ options.PropertyNamingPolicy == global::System.Text.Json.JsonNamingPolicy.SnakeCaseLower ||
+ options.PropertyNamingPolicy == global::System.Text.Json.JsonNamingPolicy.KebabCaseLower)
+ {
+ writer.WritePropertyName(lowerCaseUtf8Name);
+ return;
+ }
+
+ var convertedName = options.PropertyNamingPolicy.ConvertName(defaultName);
+ writer.WritePropertyName(convertedName);
+ }
+
+ {{Inheritdoc()}}
+ public override void Write(
+ global::System.Text.Json.Utf8JsonWriter writer,
+ {{new UnionTypeNameComponent(m)}} value,
+ global::System.Text.Json.JsonSerializerOptions options)
+ {
+ writer.WriteStartObject();
+ WriteVariantPropertyName(writer, options);
+ global::System.Text.Json.JsonSerializer.Serialize(writer, value.Variant.Kind, options);
+ WriteValuePropertyName(writer, options);
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$"""
+ global::System.Text.Json.JsonSerializer.Serialize(writer, value.CastTo{{v.Name}}, options);
+ break;
+ """
+ );
+ }, static (_, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+ b.Append("throw value.CreateUnknownVariantException();");
+ }, "value.Variant.Kind")}}
+
+ writer.WriteEndObject();
+ }
+ """
+ );
+ });
+
+ var type = Create((Model, members), static (t, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var (model, members) = t;
+
+ b.Append(
+ $"""
+ {Summary(model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Implements JSON conversion logic for serializing and deserializing instances of {Cref(m.DocsCommentId)}.");
+ })}
+ {Type(
+ "private sealed class",
+ "JsonConverter",
+ members,
+ baseTypeList:
+ [
+ TypeName(
+ $"global::System.Text.Json.Serialization.JsonConverter<{new UnionTypeNameComponent(model)}>")
+ ])}
+ """
+ );
+ });
+
+ var region = Region("Json Converter", type);
+
+ builder.Append(region);
+ }
+}
diff --git a/Janus.Analyzers/Components/MappingComponent.cs b/Janus.Analyzers/Components/MappingComponent.cs
new file mode 100644
index 0000000..d063cdf
--- /dev/null
+++ b/Janus.Analyzers/Components/MappingComponent.cs
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using System.Diagnostics.CodeAnalysis;
+using Lyra;
+using static Lyra.ComponentFactory;
+using static Lyra.ComponentFactory.Docs;
+
+internal readonly record struct MappingComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var methods = Create(Model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$"""
+ {{Summary("Maps this union to another.")}}
+ {{Param("factory", "The factory used to create the target union instance.")}}
+ {{TypeParam("TUnion", "The type of union to map to.")}}
+ {{TypeParam("TFactory", "The type of factory used to perform the mapping operation.")}}
+ {{Returns("The created union instance.")}}
+ public TUnion MapTo(
+ TFactory factory)
+ where TFactory : global::RhoMicro.CodeAnalysis.IUnionFactory
+ {
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"return factory.Create({new VariantAccessorComponent(v)});");
+ })}}
+ }
+
+ {{Summary("Attempts to map this union to another.")}}
+ {{Param("factory", "The factory used to create the target union instance.")}}
+ {{Param("union", static (b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"The union instance if one could be created; otherwise, {Langword("default")}.");
+ })}}
+ {{TypeParam("TUnion", "The type of union to map to.")}}
+ {{TypeParam("TFactory", "The type of factory used to perform the mapping operation.")}}
+ {{Returns(static (b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"{Langword("true")} if a union instance could be created; otherwise, {Langword("false")}.");
+ })}}
+ public bool TryMapTo(
+ TFactory factory,
+ [{{typeof(NotNullWhenAttribute)}}(true)]
+ out TUnion? union)
+ where TFactory : global::RhoMicro.CodeAnalysis.IUnionFactory
+ {
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"return factory.TryCreate({new VariantAccessorComponent(v)}, out union);");
+ })}}
+ }
+ """
+ );
+ });
+
+ var region = Region("Mapping", methods);
+
+ builder.Append(region);
+ }
+}
diff --git a/Janus.Analyzers/Components/OperatorsComponent.cs b/Janus.Analyzers/Components/OperatorsComponent.cs
new file mode 100644
index 0000000..73dd739
--- /dev/null
+++ b/Janus.Analyzers/Components/OperatorsComponent.cs
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using Lyra;
+using static Lyra.ComponentFactory;
+using static Lyra.ComponentFactory.Docs;
+
+internal readonly record struct OperatorsComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var operators = ComponentFactory.Create(Model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ for (var i = 0; i < m.Variants.Count; i++)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var variant = m.Variants[i];
+
+ if (variant.Type.IsInterface)
+ {
+ continue;
+ }
+
+ if (i is not 0)
+ {
+ b.AppendLine().AppendLine();
+ }
+
+ b.Append(
+ $"""
+ {Inheritdoc()}
+ public static implicit operator {new UnionTypeNameComponent(m)}({variant.Type.NullableName} value) => Create(value);
+ {Inheritdoc()}
+ public static {(m.Variants.Count is 1 ? "implicit" : "explicit")} operator {variant.Type.NullableName}({new UnionTypeNameComponent(m)} union) => union.CastTo{variant.Name};
+ """
+ );
+ }
+ });
+
+ var region = ComponentFactory.Region("Operators", operators);
+
+ builder.Append(region);
+ }
+}
diff --git a/Janus.Analyzers/Components/PropertiesComponent.cs b/Janus.Analyzers/Components/PropertiesComponent.cs
new file mode 100644
index 0000000..8d93ded
--- /dev/null
+++ b/Janus.Analyzers/Components/PropertiesComponent.cs
@@ -0,0 +1,102 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using System.Diagnostics.CodeAnalysis;
+using Lyra;
+using static Lyra.ComponentFactory;
+using static Lyra.ComponentFactory.Docs;
+
+internal readonly record struct PropertiesComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var properties = Create(Model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var isNullable = m.Variants.Any(v => v.Type is { IsNullable: true } or { Kind: VariantTypeKind.Unknown });
+
+ b.AppendLine(
+ $$"""
+ {{Summary("Gets the variant of the union.")}}
+ public VariantModel Variant { get; } = VariantModel.Unknown;
+ {{Summary("Gets the value of the union.")}}
+ public object{{(isNullable ? "?" : String.Empty)}} Value
+ {
+ get
+ {
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"return {new VariantAccessorComponent(v)};");
+ }, static (_, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append("throw CreateUnknownVariantException();");
+ })}}
+ }
+ }
+ """
+ );
+
+ foreach (var variant in m.Variants)
+ {
+ b.AppendLine(Summary(variant, static (v, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $"Gets a value indicating whether the union is of the {C(v.Name)} variant, which is of type {Cref(v.Type.DocsId)}.");
+ }))
+ .SetCondition(variant.Type.Kind is VariantTypeKind.Reference)
+ .AppendLine($"[{typeof(MemberNotNullWhenAttribute)}(true, nameof(As{variant.Name}))]")
+ .UnsetCondition()
+ .AppendLine(
+ $"""
+ public bool Is{variant.Name} => Variant.Kind is VariantKind.{variant.Name};
+ {Summary(variant, static (v, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Gets the unions value if the union is of the {C(v.Name)} variant; otherwise, {(v.Type.Kind is VariantTypeKind.Reference ? Langword("null") : Langword("default"))}.");
+ })}
+ {Remarks(variant, static (v, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"""
+ If the union is not of the {C(v.Name)} variant, no conversion is attempted.
+ In order to cast to a specific type, use {Cref("CastTo{TVariant}")} instead.
+ """);
+ })}
+ public {variant.Type.NullableName}{(variant.Type is { Kind: VariantTypeKind.Reference, IsNullable: false } ? "?" : String.Empty)} As{variant.Name} => {new VariantAccessorComponent(variant, NullableCast: true)};
+ {Summary(variant, static (v, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Gets the represented value if the union is of the {C(v.Name)} variant; otherwise, an exception is thrown.");
+ })}
+ {Remarks(variant, static (v,b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"""
+ If the union is not of the {C(v.Name)} variant, no conversion is attempted.
+ In order to cast to a specific type, use {Cref("CastTo{TVariant}")} instead.
+ """);
+ })}
+ public {variant.Type.NullableName} CastTo{variant.Name} => Variant.Kind is VariantKind.{variant.Name} ? {new VariantAccessorComponent(variant)} : throw CreateInvalidCastException(VariantKind.{variant.Name});
+ """);
+ }
+ });
+
+ var region = Region("Properties", properties);
+
+ builder.Append(region);
+ }
+}
diff --git a/Janus.Analyzers/Components/SwitchComponent.cs b/Janus.Analyzers/Components/SwitchComponent.cs
new file mode 100644
index 0000000..4f77ec0
--- /dev/null
+++ b/Janus.Analyzers/Components/SwitchComponent.cs
@@ -0,0 +1,413 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using Lyra;
+using static Lyra.ComponentFactory;
+using static Lyra.ComponentFactory.Docs;
+
+internal readonly record struct SwitchComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var methods = Create(Model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var statelessHandler = Create(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$"""
+ {{SwitchSummary()}}
+ {{SwitchHandlerParams(m)}}
+ public void Switch(
+ {{List(m.Variants, static (v, _, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"{typeof(Action)}<{v.Type.NullableName}> on{v.Name}");
+ }, separator: ",\n")
+ }})
+ {
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $"""
+ on{v.Name}.Invoke({new VariantAccessorComponent(v)});
+ return;
+ """);
+ })
+ }}
+ }
+ """);
+ });
+
+ var statefulHandler = Create(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$"""
+ {{SwitchSummary()}}
+ {{SwitchStateParam()}}
+ {{SwitchHandlerParams(m)}}
+ {{SwitchStateTypeparam()}}
+ public void Switch(
+ TState state,
+ {{List(m.Variants, static (v, _, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"{typeof(Action)}<{v.Type.NullableName}, TState> on{v.Name}");
+ }, separator: ",\n")}})
+ {
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $"""
+ on{v.Name}.Invoke({new VariantAccessorComponent(v)}, state);
+ return;
+ """);
+ })}}
+ }
+ """);
+ });
+
+ var statelessDefaultHandler = Create(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$"""
+ {{SwitchSummary()}}
+ {{SwitchDefaultHandlerParam()}}
+ {{SwitchHandlerParams(m)}}
+ public void Switch(
+ {{typeof(Action)}} defaultHandler,
+ {{List(m.Variants, static (v, _, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"{typeof(Action)}<{v.Type.NullableName}>? on{v.Name} = null");
+ }, separator: ",\n")}})
+ {
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($$"""
+ if(on{{v.Name}} is not null)
+ {
+ on{{v.Name}}.Invoke({{new VariantAccessorComponent(v)}});
+ }
+ else
+ {
+ defaultHandler.Invoke();
+ }
+
+ return;
+ """
+ );
+ })}}
+ }
+ """);
+ });
+
+ var statefulDefaultHandler = Create(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$"""
+ {{SwitchSummary()}}
+ {{SwitchStateParam()}}
+ {{SwitchDefaultHandlerParam()}}
+ {{SwitchHandlerParams(m)}}
+ {{SwitchStateTypeparam()}}
+ public void Switch(
+ TState state,
+ {{typeof(Action)}} defaultHandler,
+ {{List(m.Variants, static (v, _, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"{typeof(Action)}<{v.Type.NullableName}, TState>? on{v.Name} = null");
+ }, separator: ",\n")}})
+ {
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($$"""
+ if(on{{v.Name}} is not null)
+ {
+ on{{v.Name}}.Invoke({{new VariantAccessorComponent(v)}}, state);
+ }
+ else
+ {
+ defaultHandler.Invoke(state);
+ }
+
+ return;
+ """
+ );
+ })}}
+ }
+ """);
+ });
+
+ const String func = "global::System.Func";
+
+ var statelessProjection = Create(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$"""
+ {{SwitchSummary()}}
+ {{SwitchHandlerParams(m)}}
+ {{SwitchResultTypeparam()}}
+ {{SwitchReturns()}}
+ public TResult Switch(
+ {{List(m.Variants, static (v, _, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"{func}<{v.Type.NullableName}, TResult> on{v.Name}");
+ }, separator: ",\n")}})
+ {
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"return on{v.Name}.Invoke({new VariantAccessorComponent(v)});");
+ })}}
+ }
+ """);
+ });
+
+ var statefulProjection = Create(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$"""
+ {{SwitchSummary()}}
+ {{SwitchStateParam()}}
+ {{SwitchHandlerParams(m)}}
+ {{SwitchStateTypeparam()}}
+ {{SwitchResultTypeparam()}}
+ {{SwitchReturns()}}
+ public TResult Switch(
+ TState state,
+ {{List(m.Variants, static (v, _, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"{func}<{v.Type.NullableName}, TState, TResult> on{v.Name}");
+ }, separator: ",\n")}})
+ {
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"return on{v.Name}.Invoke({new VariantAccessorComponent(v)}, state);");
+ })}}
+ }
+ """);
+ });
+
+ var statelessDefaultProjection = Create(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$"""
+ {{SwitchSummary()}}
+ {{SwitchDefaultHandlerParam()}}
+ {{SwitchHandlerParams(m)}}
+ {{SwitchResultTypeparam()}}
+ {{SwitchReturns()}}
+ public TResult Switch(
+ {{func}} defaultHandler,
+ {{List(m.Variants, static (v, _, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"{func}<{v.Type.NullableName}, TResult>? on{v.Name} = null");
+ }, separator: ",\n")}})
+ {
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($$"""
+ if(on{{v.Name}} is not null)
+ {
+ return on{{v.Name}}.Invoke({{new VariantAccessorComponent(v)}});
+ }
+ else
+ {
+ return defaultHandler.Invoke();
+ }
+ """
+ );
+ })}}
+ }
+ """);
+ });
+
+ var statefulDefaultProjection = Create(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$"""
+ {{SwitchSummary()}}
+ {{SwitchStateParam()}}
+ {{SwitchDefaultHandlerParam()}}
+ {{SwitchHandlerParams(m)}}
+ {{SwitchStateTypeparam()}}
+ {{SwitchResultTypeparam()}}
+ {{SwitchReturns()}}
+ public TResult Switch(
+ TState state,
+ {{func}} defaultHandler,
+ {{List(m.Variants, static (v, _, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"{func}<{v.Type.NullableName}, TState, TResult>? on{v.Name} = null");
+ }, separator: ",\n")}})
+ {
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($$"""
+ if(on{{v.Name}} is not null)
+ {
+ return on{{v.Name}}.Invoke({{new VariantAccessorComponent(v)}}, state);
+ }
+ else
+ {
+ return defaultHandler.Invoke(state);
+ }
+ """
+ );
+ })}}
+ }
+ """);
+ });
+
+ var statelessDefaultResultProjection = Create(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$"""
+ {{SwitchSummary()}}
+ {{SwitchDefaultResultParam()}}
+ {{SwitchHandlerParams(m)}}
+ {{SwitchResultTypeparam()}}
+ {{SwitchReturns()}}
+ public TResult Switch(
+ TResult defaultResult,
+ {{List(m.Variants, static (v, _, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"{func}<{v.Type.NullableName}, TResult>? on{v.Name} = null");
+ }, separator: ",\n")}})
+ {
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($$"""
+ if(on{{v.Name}} is not null)
+ {
+ return on{{v.Name}}.Invoke({{new VariantAccessorComponent(v)}});
+ }
+ else
+ {
+ return defaultResult;
+ }
+ """
+ );
+ })}}
+ }
+ """);
+ });
+
+ var statefulDefaultResultProjection = Create(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$"""
+ {{SwitchSummary()}}
+ {{SwitchStateParam()}}
+ {{SwitchDefaultResultParam()}}
+ {{SwitchHandlerParams(m)}}
+ {{SwitchStateTypeparam()}}
+ {{SwitchResultTypeparam()}}
+ {{SwitchReturns()}}
+ public TResult Switch(
+ TState state,
+ TResult defaultResult,
+ {{List(m.Variants, static (v, _, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"{func}<{v.Type.NullableName}, TState, TResult>? on{v.Name} = null");
+ }, separator: ",\n")}})
+ {
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($$"""
+ if(on{{v.Name}} is not null)
+ {
+ return on{{v.Name}}.Invoke({{new VariantAccessorComponent(v)}}, state);
+ }
+ else
+ {
+ return defaultResult;
+ }
+ """
+ );
+ })}}
+ }
+ """);
+ });
+
+ b.Append($"""
+ {statelessHandler}
+ {statelessDefaultHandler}
+ {statefulHandler}
+ {statefulDefaultHandler}
+ {statelessProjection}
+ {statelessDefaultProjection}
+ {statelessDefaultResultProjection}
+ {statefulProjection}
+ {statefulDefaultProjection}
+ {statefulDefaultResultProjection}
+ """
+ );
+ });
+
+ var region = Region("Switch", methods);
+
+ builder.Append(region);
+ }
+}
diff --git a/Janus.Analyzers/Components/ToStringComponent.cs b/Janus.Analyzers/Components/ToStringComponent.cs
new file mode 100644
index 0000000..c8085a5
--- /dev/null
+++ b/Janus.Analyzers/Components/ToStringComponent.cs
@@ -0,0 +1,110 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using Lyra;
+using static Lyra.ComponentFactory;
+using static Lyra.ComponentFactory.Docs;
+
+internal readonly record struct ToStringComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (Model.IsToStringUserProvided || Model.Settings.ToStringSetting is ToStringSetting.None)
+ {
+ return;
+ }
+
+ switch (Model.Settings.ToStringSetting)
+ {
+ case ToStringSetting.Simple:
+ AppendSimple(builder);
+ break;
+ case ToStringSetting.Detailed:
+ AppendDetailed(builder);
+ break;
+ }
+ }
+
+ private void AppendDetailed(CSharpSourceBuilder builder) =>
+ builder.Append(
+ $$"""
+ {{Inheritdoc()}}
+ public override string ToString()
+ {
+ {{new VariantsSwitchComponent(Model, static (v, m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$$"""
+ return $"{{{ComponentFactory.Create(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(m.Name);
+
+ if (m.TypeParameters is [])
+ {
+ return;
+ }
+
+ b.Append($"<{ComponentFactory.List(m.TypeParameters, static (p, _, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$"""
+ {{p}}{(typeof({{p}}).IsGenericTypeParameter ? string.Empty : $":{typeof({{p}})}" )}
+ """
+ );
+ }, separator: ", ")}>");
+ })}}} {{ Variants: [{{{ComponentFactory.Create((v, m), static (t, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var (variant, model) = t;
+
+ for (var i = 0; i < model.Variants.Count; i++)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var isCaseVariant = ReferenceEquals(variant.Name, model.Variants[i].Name);
+
+ b.SetCondition(i is not 0)
+ .Append(", ")
+ .UnsetCondition()
+ .SetCondition(isCaseVariant)
+ .Append('<')
+ .UnsetCondition()
+ .Append(model.Variants[i].Name)
+ .SetCondition(isCaseVariant)
+ .Append('>')
+ .UnsetCondition();
+ }
+ })}}}], Value: {Value} }}";
+ """
+ );
+ })}}
+ }
+ """
+ );
+
+ private void AppendSimple(CSharpSourceBuilder builder) =>
+ builder.Append(
+ $$"""
+ {{Inheritdoc()}}
+ public override string ToString()
+ {
+ {{new VariantsSwitchComponent(Model, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"return {new VariantAccessorComponent(v)}.ToString();");
+ })}}
+ }
+ """
+ );
+}
diff --git a/Janus.Analyzers/Components/UnionComponent.cs b/Janus.Analyzers/Components/UnionComponent.cs
new file mode 100644
index 0000000..20cf4e4
--- /dev/null
+++ b/Janus.Analyzers/Components/UnionComponent.cs
@@ -0,0 +1,176 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using Lyra;
+using static Lyra.ComponentFactory;
+using static Lyra.ComponentFactory.Docs;
+
+internal readonly record struct UnionComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ var body = Create(
+ Model,
+ static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (m.Settings.JsonConverterSetting is JsonConverterSetting.EmitJsonConverter)
+ {
+ b.AppendLine(new JsonConverterComponent(m));
+ }
+
+ b.AppendLine(
+ $"""
+ {new VariantGroupKindsComponent(m)}
+ {new VariantGroupModelComponent(m)}
+ {new VariantKindComponent(m)}
+ {new VariantModelComponent(m)}
+ {new FactoryComponent(m)}
+ """);
+
+ if (ContainsUnmanagedVariants(m))
+ {
+ b.AppendLine(new UnmanagedVariantsContainerComponent(m));
+ }
+
+ b.Append(
+ $"""
+ {new ConstructorComponent(m)}
+ {new FieldsComponent(m)}
+ {new PropertiesComponent(m)}
+ {new SwitchComponent(m)}
+ {new InspectionsComponent(m)}
+ {new ValidationComponent(m)}
+ {new FactoriesComponent(m)}
+ {new MappingComponent(m)}
+ {new EqualityComponent(m)}
+ {new OperatorsComponent(m)}
+ """);
+ });
+
+ var type = Create((Model, body), static (t, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var (model, body) = t;
+
+ if (model.EmitDocsComment)
+ {
+ b.AppendLine(Summary(model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"""
+ Implements a tagged union for the following variant types:
+ {List("table", m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(ListHeader("Name", "Type and Description"));
+
+ for (var i = 0; i < m.Variants.Count; i++)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var variant = m.Variants[i];
+ b.Append($"""
+
+ {Item(variant, static (v, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(v.Name);
+ }, static (v, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (v.Description is [_, ..])
+ {
+ b.Append("Type: ");
+ }
+
+ b.Append(Cref(v.Type.DocsId));
+
+ if (v.Description is [_, ..])
+ {
+ b.Append($"""
+ {Br()}
+ Description: {v.Description}
+ """);
+ }
+ })}
+ """);
+ }
+ })}
+ """);
+ }));
+ }
+
+ if (model.Settings.JsonConverterSetting is JsonConverterSetting.EmitJsonConverter)
+ {
+ b.AppendLine(
+ $"[global::System.Text.Json.Serialization.JsonConverterAttribute(typeof({new UnionTypeNameComponent(model, RenderOpenGeneric: true)}.JsonConverter))]");
+ }
+
+ b.Append(Type(
+ model.TypeKind is UnionTypeKind.Class ? "partial class" : "partial struct",
+ model.Name,
+ body,
+ baseTypeList:
+ [
+ model.TypeNames.IUnion,
+ TypeName(
+ $"global::System.IEquatable<{new UnionTypeNameComponent(model)}>")
+ ],
+ typeParameters: [..model.TypeParameters]));
+ });
+
+ var containingTypes = Create((Model, type), static (t, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var (model, type) = t;
+
+ foreach (var containingType in model.ContainingTypes)
+ {
+ b.Append($"partial {containingType.Modifier} {containingType.Name}");
+ if (containingType.TypeParameters is not [])
+ {
+ b.Append('<')
+ .Append(List(containingType.TypeParameters, separator: ", "))
+ .Append('>');
+ }
+
+ b.AppendLine().AppendLine('{').Indent();
+ }
+
+ b.AppendLine(type);
+
+ for (var i = 0; i < model.ContainingTypes.Count; i++)
+ {
+ b.Detent().AppendLine('}');
+ }
+ });
+
+ var @namespace = Namespace(
+ Model.Namespace,
+ containingTypes);
+
+ builder.Append(@namespace);
+ }
+
+ private static Boolean ContainsUnmanagedVariants(UnionModel m)
+ {
+ foreach (var variant in m.Variants)
+ {
+ if (variant.Type.Kind is VariantTypeKind.Unmanaged)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/Janus.Analyzers/Components/UnionTypeMetadataNameComponent.cs b/Janus.Analyzers/Components/UnionTypeMetadataNameComponent.cs
new file mode 100644
index 0000000..90be655
--- /dev/null
+++ b/Janus.Analyzers/Components/UnionTypeMetadataNameComponent.cs
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using Lyra;
+
+internal readonly record struct UnionTypeMetadataNameComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (Model.Namespace is not [])
+ {
+ builder.Append($"{Model.Namespace}.");
+ }
+
+ foreach (var containingType in Model.ContainingTypes)
+ {
+ builder.Append(containingType.Name);
+
+ if (containingType.TypeParameters is { Count: > 0 and var containingTypeParameterCount })
+ {
+ builder.Append($"`{containingTypeParameterCount}");
+ }
+
+ builder.Append('.');
+ }
+
+ builder.Append(Model.Name);
+
+ if (Model.TypeParameters is { Count: > 0 and var count })
+ {
+ builder.Append($"`{count}");
+ }
+ }
+}
diff --git a/Janus.Analyzers/Components/UnionTypeNameComponent.cs b/Janus.Analyzers/Components/UnionTypeNameComponent.cs
new file mode 100644
index 0000000..7d4786e
--- /dev/null
+++ b/Janus.Analyzers/Components/UnionTypeNameComponent.cs
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using System.Buffers;
+using System.Runtime.InteropServices;
+using System.Text;
+using Lyra;
+using static Lyra.ComponentFactory;
+
+internal readonly record struct UnionTypeNameComponent(
+ UnionModel Model,
+ Boolean RenderOpenGeneric = false,
+ Boolean RenderNullable = false)
+ : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ builder.Append(Model.Name);
+
+ if (Model.TypeParameters is [])
+ {
+ if (RenderNullable && Model.TypeKind is UnionTypeKind.Class)
+ {
+ builder.Append('?');
+ }
+
+ return;
+ }
+
+ var rented = RenderOpenGeneric
+ ? ArrayPool.Shared.Rent(Model.TypeParameters.Count)
+ : null;
+ try
+ {
+ var emptyNames = rented;
+ if (emptyNames is not null)
+ {
+ if (emptyNames.Length != Model.TypeParameters.Count)
+ {
+ emptyNames = new String[Model.TypeParameters.Count];
+ }
+
+ for (var i = 0; i < emptyNames.Length; i++)
+ {
+ emptyNames[i] = String.Empty;
+ }
+ }
+
+ var typeParameters = RenderOpenGeneric
+ ? emptyNames!
+ : [..Model.TypeParameters];
+
+ builder.Append($"<{List(typeParameters, static (p, _, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(p);
+ }, separator: RenderOpenGeneric ? "," : ", ")}>");
+
+ if (RenderNullable && Model.TypeKind is UnionTypeKind.Class)
+ {
+ builder.Append('?');
+ }
+ }
+ finally
+ {
+ if (rented is not null)
+ {
+ ArrayPool.Shared.Return(rented);
+ }
+ }
+ }
+
+ public override String ToString()
+ {
+ var builder = new StringBuilder();
+
+ builder.Append(Model.Name);
+
+ if (Model.TypeParameters is [])
+ {
+ if (RenderNullable && Model.TypeKind is UnionTypeKind.Class)
+ {
+ builder.Append('?');
+ }
+
+ return builder.ToString();
+ }
+
+ var rented = RenderOpenGeneric
+ ? ArrayPool.Shared.Rent(Model.TypeParameters.Count)
+ : null;
+ try
+ {
+ var emptyNames = rented;
+ if (emptyNames is not null)
+ {
+ if (emptyNames.Length != Model.TypeParameters.Count)
+ {
+ emptyNames = new String[Model.TypeParameters.Count];
+ }
+
+ for (var i = 0; i < emptyNames.Length; i++)
+ {
+ emptyNames[i] = String.Empty;
+ }
+ }
+
+ var typeParameters = RenderOpenGeneric
+ ? emptyNames!
+ : [..Model.TypeParameters];
+
+ builder.Append('<');
+
+ for (var i = 0; i < typeParameters.Length; i++)
+ {
+ if (i is not 0)
+ {
+ builder.Append(RenderOpenGeneric ? "," : ", ");
+ }
+
+ builder.Append(typeParameters[i]);
+ }
+
+ builder.Append('>');
+
+ if (RenderNullable && Model.TypeKind is UnionTypeKind.Class)
+ {
+ builder.Append('?');
+ }
+ }
+ finally
+ {
+ if (rented is not null)
+ {
+ ArrayPool.Shared.Return(rented);
+ }
+ }
+
+ if (RenderNullable && Model.TypeKind is UnionTypeKind.Class)
+ {
+ builder.Append('?');
+ }
+
+ return builder.ToString();
+ }
+}
diff --git a/Janus.Analyzers/Components/UnmanagedVariantsContainerComponent.cs b/Janus.Analyzers/Components/UnmanagedVariantsContainerComponent.cs
new file mode 100644
index 0000000..0403aaf
--- /dev/null
+++ b/Janus.Analyzers/Components/UnmanagedVariantsContainerComponent.cs
@@ -0,0 +1,95 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using System.Runtime.InteropServices;
+using Lyra;
+using static Lyra.ComponentFactory;
+using static Lyra.ComponentFactory.Docs;
+
+internal readonly record struct UnmanagedVariantsContainerComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var body = Create(
+ Model,
+ static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ foreach (var variant in m.Variants)
+ {
+ if (variant.Type.Kind is not VariantTypeKind.Unmanaged)
+ {
+ continue;
+ }
+
+ b.AppendLine(
+ $$"""
+ {{Summary("Initializes a new instance.")}}
+ {{Param("value", "The value to store.")}}
+ public UnmanagedVariantsContainer({{variant.Type.NullableName}} value)
+ {
+ {{variant.Name}} = value;
+ }
+ {{Summary(variant, static (v, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Gets the stored value typed as {Cref(v.Type.DocsId)}, to be used when representing the {C(v.Name)} variant.");
+ })}}
+ {{Create(m, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (m.TypeParameters is not [])
+ {
+ return;
+ }
+
+ b.Append(Attribute(TypeName(), PositionalAttributeArgument("0")));
+ })}}
+ public readonly {{variant.Type.NullableName}} {{variant.Name}};
+ """
+ );
+ }
+ });
+
+ var type = Create((Model, body), static (t, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var (model, body) = t;
+
+ b.Append($"""
+ {Summary(model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Stores unmanaged variants of {Cref(m.DocsCommentId)}.");
+ })}
+ {Type(
+ "private readonly struct",
+ "UnmanagedVariantsContainer",
+ body,
+ attributes:
+ model.TypeParameters is []
+ ?
+ [
+ Attribute(
+ TypeName(),
+ AttributeArgumentComponent.CreatePositionalMemberAccess(
+ TypeName(),
+ nameof(LayoutKind.Explicit)))
+ ]
+ : [])}
+ """);
+ });
+
+ var region = Region("UnmanagedVariantsContainer", type);
+
+ builder.Append(region);
+ }
+}
diff --git a/Janus.Analyzers/Components/ValidationComponent.cs b/Janus.Analyzers/Components/ValidationComponent.cs
new file mode 100644
index 0000000..a6c389a
--- /dev/null
+++ b/Janus.Analyzers/Components/ValidationComponent.cs
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using Lyra;
+using static Lyra.ComponentFactory;
+using static Lyra.ComponentFactory.Docs;
+
+internal readonly record struct ValidationComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var methods = Create(Model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$"""
+ {{Summary("Creates an exception used when converting to a given variant.")}}
+ {{Param("variant", "The variant a conversion was attempted to.")}}
+ {{Returns("The created exception.")}}
+ private {{typeof(InvalidCastException)}} CreateInvalidCastException(VariantKind variant)
+ => new {{typeof(InvalidCastException)}}(
+ $"Unable to convert union to '{new VariantModel(variant).Name}', as it is currently representing " +
+ $"the '{Variant}' variant.");
+
+ {{Summary("Creates an exception used when encountering an unknown variant state.")}}
+ {{Returns("The created exception.")}}
+ private {{typeof(InvalidOperationException)}} CreateUnknownVariantException()
+ => new {{typeof(InvalidOperationException)}}(
+ $"Unable to determine the variant of this union, as '{nameof(Variant)}' is not " +
+ $"representing a valid variant of this union: '{Variant}'. This could be either " +
+ "because the union itself was not initialized correctly, or due to a bug in the " +
+ "'JanusJanus' source generator that generated this union type. Please report an issue to the " +
+ "maintainer.");
+
+ {{List(m.Variants, static (v, _, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $"""
+ {Summary("Validates a variant value for creating a new instance of the union.")}
+ {Remarks(static (b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $"""
+ If {ParamRef("throwIfInvalid")} is {Langword("true")}, the implementation should throw an
+ exception outlining why {ParamRef("value")} is invalid.
+ Otherwise, {ParamRef("isValid")} should be assigned {Langword("true")} if
+ {ParamRef("value")} is valid and {Langword("false")} if it is not.
+ """);
+ })}
+ {Param("value", "The value to validate.")}
+ {Param("throwIfInvalid", "Indicates whether to throw an exception if the value is invalid.")}
+ {Param("isValid", "Indicates whether the value is valid.")}
+ static partial void Validate({v.Type.NullableName} value, bool throwIfInvalid, ref bool isValid);
+ """
+ );
+ }, separator: "\n\n")}}
+ """
+ );
+ });
+
+ var region = Region("Validation", methods);
+
+ builder.Append(region);
+ }
+}
diff --git a/Janus.Analyzers/Components/VariantAccessorComponent.cs b/Janus.Analyzers/Components/VariantAccessorComponent.cs
new file mode 100644
index 0000000..a110ca6
--- /dev/null
+++ b/Janus.Analyzers/Components/VariantAccessorComponent.cs
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using Lyra;
+
+internal readonly record struct VariantAccessorComponent(
+ UnionTypeAttribute.Model Model,
+ String InstanceExpression = "this",
+ Boolean NullableCast = false)
+ : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ switch (Model.Type.Kind)
+ {
+ case VariantTypeKind.Value or VariantTypeKind.Unknown:
+ builder.Append($"{InstanceExpression}._{Model.Name}");
+ break;
+ case VariantTypeKind.Unmanaged:
+ builder.Append($"{InstanceExpression}._unmanagedVariantsContainer.{Model.Name}");
+ break;
+ case VariantTypeKind.Reference:
+ if (NullableCast && !Model.Type.IsNullable)
+ {
+ builder.Append($"({Model.Type.Name}?){InstanceExpression}._referenceVariantsContainer");
+ }
+ else
+ {
+ builder.Append($"(({Model.Type.NullableName}){InstanceExpression}._referenceVariantsContainer!)");
+ }
+
+ break;
+ }
+ }
+}
diff --git a/Janus.Analyzers/Components/VariantGroupKindsComponent.cs b/Janus.Analyzers/Components/VariantGroupKindsComponent.cs
new file mode 100644
index 0000000..8696f84
--- /dev/null
+++ b/Janus.Analyzers/Components/VariantGroupKindsComponent.cs
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using System.Runtime.CompilerServices;
+using Lyra;
+using static Lyra.ComponentFactory;
+using static Lyra.ComponentFactory.Docs;
+
+internal readonly record struct VariantGroupKindsComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var members = List(
+ Model.VariantGroups,
+ static (g, i, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.AppendLine(Summary(g, static (g, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Represents the {g} group.");
+ }));
+
+ if (i is 0)
+ {
+ b.Append($"{g} = 0");
+ }
+ else
+ {
+ b.Append($"{g} = 1 << {i - 1}");
+ }
+ },
+ separator: ",\n");
+ var type = Create((Model, members), static (t, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var (model, members) = t;
+
+ b.Append(
+ $"""
+ {Summary(model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Enumerates the kinds of groups variants of the {Cref(m.DocsCommentId)} may be a part of.");
+ })}
+ {Type(
+ "public enum",
+ "VariantGroupKinds",
+ members,
+ attributes:
+ [
+ Attribute(TypeName(), arguments: [])
+ ],
+ baseTypeList: [FlagsEnumBackingType(model.VariantGroups.Count - 1)])}
+ """
+ );
+ });
+ var region = Region("VariantGroupKinds", type);
+ builder.Append(region);
+ }
+}
diff --git a/Janus.Analyzers/Components/VariantGroupModelComponent.cs b/Janus.Analyzers/Components/VariantGroupModelComponent.cs
new file mode 100644
index 0000000..3c20942
--- /dev/null
+++ b/Janus.Analyzers/Components/VariantGroupModelComponent.cs
@@ -0,0 +1,280 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using Lyra;
+using static Lyra.ComponentFactory;
+using static Lyra.ComponentFactory.Docs;
+
+internal readonly record struct VariantGroupModelComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var members = Create(Model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$"""
+ {{Summary("Initializes a new instance.")}}
+ {{Param("kinds", "The kinds of variant groups to be included in the group set.")}}
+ public VariantGroupModel(VariantGroupKinds kinds)
+ {
+ Kinds = kinds;
+ }
+
+ {{Summary("Gets the kinds of groups contained in the group.")}}
+ public VariantGroupKinds Kinds { get; }
+
+ {{List(m.VariantGroups, static (g, _, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$"""
+ {{Summary(g, static (g, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Gets the group model representing the {g} group.");
+ })}}
+ public static VariantGroupModel {{g}} { get; } = new(VariantGroupKinds.{{g}});
+ """
+ );
+ }, separator: "\n")}}
+
+ {{List(m.VariantGroups, static (g, _, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (g is "None")
+ {
+ return;
+ }
+
+ b.AppendLine(
+ $"""
+ {Summary(g, static (g, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Gets a value indicating whether this group contains the {g} group.");
+ })}
+ public bool Contains{g} => Contains(VariantGroupModel.{g});
+ """
+ );
+ })}}
+ {{Summary("Gets a value indicating whether this group contains the provided group.")}}
+ {{Param("group", "The group to check is contained in this group.")}}
+ {{Returns(static (b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"{Langword("true")} if this group contains {ParamRef("group")}; otherwise, {Langword("false")}.");
+ })}}
+ public bool Contains(VariantGroupModel group) => (Kinds & group.Kinds) == group.Kinds;
+
+ {{Summary("Gets the set of all available groups.")}}
+ public static global::System.Collections.Immutable.ImmutableArray AllValues =>
+ [
+ {{List(m.VariantGroups, static (g, _, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(g);
+ }, separator: ",\n")}}
+ ];
+
+ {{Summary("Gets the amount of individual groups contained in this group.")}}
+ public int IndividualGroupCount => PopCount((uint)Kinds);
+
+ private void GetIndividualGroups(global::System.Span buffer)
+ {
+ var count = IndividualGroupCount;
+
+ if (count is 0)
+ {
+ return;
+ }
+
+ if (buffer.Length < count)
+ {
+ throw new global::System.ArgumentOutOfRangeException(
+ nameof(buffer),
+ $"{nameof(buffer)} did not have the required length of IndividualGroupCount. The required length was {count}, but the span provided had a length of {buffer.Length}.");
+ }
+
+ if (count is 1)
+ {
+ buffer[0] = this;
+ return;
+ }
+
+ var groupIndex = 0;
+ for (var i = LeadingZeroCount() + 1; i < 33 && groupIndex < count; i++)
+ {
+ var flagPosition = 32 - i;
+ var flag = 1 << flagPosition;
+ if (((int)Kinds & flag) == flag)
+ {
+ buffer[groupIndex++] = new VariantGroupModel((VariantGroupKinds)flag);
+ }
+ }
+ }
+
+ {{Summary("Gets the individual groups contained in this group.")}}
+ {{Returns("The individual groups contained in this group.")}}
+ public global::System.Collections.Immutable.ImmutableArray GetIndividualGroups()
+ {
+ var groups = new VariantGroupModel[IndividualGroupCount];
+ GetIndividualGroups(groups);
+ var result = global::System.Runtime.InteropServices.ImmutableCollectionsMarshal.AsImmutableArray(groups);
+
+ return result;
+ }
+
+ {{Summary("Gets the name of the group.")}}
+ public string Name
+ {
+ get
+ {
+ var count = IndividualGroupCount;
+
+ if (count is 0 or 1)
+ {
+ return DegenerateName;
+ }
+
+ global::System.Span
+ groups = stackalloc VariantGroupModel[count]; // Count never exceeds 32
+ GetIndividualGroups(groups);
+ var builder = new global::System.Text.StringBuilder();
+ for (var i = 0; i < count; i++)
+ {
+ var group = groups[i];
+ var name = group.DegenerateName;
+
+ if (i is not 0)
+ {
+ builder.Append(" | ");
+ }
+
+ builder.Append(name);
+ }
+
+ var result = builder.ToString();
+
+ return result;
+ }
+ }
+
+ private string DegenerateName
+ {
+ get
+ {
+ switch (Kinds)
+ {
+ {{List(m.VariantGroups, static (v, _, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$"""
+ case VariantGroupKinds.{{v}}:
+ {
+ return nameof(VariantGroupKinds.{{v}});
+ }
+ """
+ );
+ }, separator: "\n")
+ }}
+ default:
+ {
+ throw CreateInvalidKindsException();
+ }
+ }
+ }
+ }
+
+ private global::System.InvalidOperationException CreateInvalidKindsException()
+ => new($"The {nameof(VariantGroupModel)} instance was not initialized correctly and is holding an invalid value: {Kinds}");
+
+ {{Inheritdoc()}}
+ public override string ToString() => Kinds.ToString();
+
+ private static global::System.ReadOnlySpan Log2DeBruijn =>
+ [
+ 00, 09, 01, 10, 13, 21, 02, 29,
+ 11, 14, 16, 18, 22, 25, 03, 30,
+ 08, 12, 20, 28, 15, 17, 24, 07,
+ 19, 27, 23, 06, 26, 05, 04, 31
+ ];
+
+ private int LeadingZeroCount()
+ {
+ var value = (uint)Kinds;
+
+ if (value == 0)
+ {
+ return 32;
+ }
+
+ value |= value >> 01;
+ value |= value >> 02;
+ value |= value >> 04;
+ value |= value >> 08;
+ value |= value >> 16;
+
+ var result = 31 ^ global::System.Runtime.CompilerServices.Unsafe.AddByteOffset(
+ ref global::System.Runtime.InteropServices.MemoryMarshal.GetReference(Log2DeBruijn),
+ ({{typeof(IntPtr)}})(int)((value * 0x07C4ACDDu) >> 27));
+
+ return result;
+ }
+
+ private static int PopCount(uint value)
+ {
+ const uint c1 = 0x_55555555u;
+ const uint c2 = 0x_33333333u;
+ const uint c3 = 0x_0F0F0F0Fu;
+ const uint c4 = 0x_01010101u;
+
+ value -= (value >> 1) & c1;
+ value = (value & c2) + ((value >> 2) & c2);
+ value = (((value + (value >> 4)) & c3) * c4) >> 24;
+
+ return (int)value;
+ }
+ """
+ );
+ });
+
+ var type = Create((Model, members), static (t, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var (model, members) = t;
+
+ b.Append(
+ $"""
+ {Summary(model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Provides strongly typed access to the group of a variant of {Cref(m.DocsCommentId)}.");
+ })}
+ {Type(
+ "public readonly record struct",
+ "VariantGroupModel",
+ members)}
+ """
+ );
+ });
+
+ var region = Region("VariantGroupModel", type);
+
+ builder.Append(region);
+ }
+}
diff --git a/Janus.Analyzers/Components/VariantKindComponent.cs b/Janus.Analyzers/Components/VariantKindComponent.cs
new file mode 100644
index 0000000..6d3ebf7
--- /dev/null
+++ b/Janus.Analyzers/Components/VariantKindComponent.cs
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using Lyra;
+using static Lyra.ComponentFactory;
+using static Lyra.ComponentFactory.Docs;
+
+internal readonly record struct VariantKindComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var members = List(
+ Model.Variants,
+ static (v, i, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (i is 0)
+ {
+ b.AppendLine(
+ $"""
+ {Summary("Represents a not yet or incorrectly initialized union.")}
+ Unknown = 0,
+ """);
+ }
+
+ b.Append(
+ $"""
+ {Summary(v, static (v, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Represents the {C(v.Name)} variant.");
+ })}
+ {v.Name} = {i + 1}
+ """);
+ },
+ separator: ",\n");
+
+ var type = Create((members, Model), static (t, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var (members, model) = t;
+
+ b.Append($"""
+ {Summary(model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Enumerates the variants of {Cref(m.DocsCommentId)}.");
+ })}
+ {Type(
+ "public enum",
+ "VariantKind",
+ members,
+ baseTypeList: [EnumBackingType(model.Variants.Count + 1)])}
+ """);
+ });
+
+ var region = Region("VariantKind", type);
+
+ builder.Append(region);
+ }
+}
diff --git a/Janus.Analyzers/Components/VariantModelComponent.cs b/Janus.Analyzers/Components/VariantModelComponent.cs
new file mode 100644
index 0000000..c2112c0
--- /dev/null
+++ b/Janus.Analyzers/Components/VariantModelComponent.cs
@@ -0,0 +1,171 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using Lyra;
+using static Lyra.ComponentFactory;
+using static Lyra.ComponentFactory.Docs;
+
+internal readonly record struct VariantModelComponent(UnionModel Model) : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var members = Create(Model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(
+ $$"""
+ {{Summary("Initializes a new instance.")}}
+ {{Param("kind", "The kind of variant represented.")}}
+ public VariantModel(VariantKind kind)
+ {
+ Kind = kind;
+ }
+
+ {{Summary("Gets the kind of variant represented.")}}
+ public VariantKind Kind { get; }
+
+ {{Summary(static (b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+ b.Append($"Gets a variant model representing {Cref("VariantKind.Unknown")}.");
+ })}}
+ public static VariantModel Unknown { get; } = new(VariantKind.Unknown);
+
+ {{List(m.Variants, static (v, _, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($$"""
+ {{Summary(v.Name, static (n, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+ b.Append($"Gets a variant model representing {Cref(n, static (n, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"VariantKind.{n}");
+ })}.");
+ })}}
+ public static VariantModel {{v.Name}} { get; } = new(VariantKind.{{v.Name}});
+ """);
+ }, "\n")}}
+
+ {{Summary("Gets the set of all variants.")}}
+ public static global::System.Collections.Immutable.ImmutableArray AllValues =>
+ [
+ {{List(m.Variants, static (v, _, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append(v.Name);
+ }, ",\n")}}
+ ];
+
+ {{Summary("Gets the name of the variant.")}}
+ public string Name
+ {
+ get
+ {
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"return nameof(VariantKind.{v.Name});");
+ },
+ VariantExpression: "Kind",
+ Default: static (_, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append("throw CreateInvalidKindException();");
+ })}}
+ }
+ }
+
+ {{Summary("Gets the type of the variant.")}}
+ public global::System.Type Type
+ {
+ get
+ {
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"return typeof({(v.Type.Kind is VariantTypeKind.Reference ? v.Type.Name : v.Type.NullableName)});");
+ },
+ VariantExpression: "Kind",
+ Default: static (_, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append("throw CreateInvalidKindException();");
+ })}}
+ }
+ }
+
+ {{Summary("Gets the groups the variant is a part of.")}}
+ public VariantGroupModel Group
+ {
+ get
+ {
+ {{new VariantsSwitchComponent(m, static (v, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"return new VariantGroupModel({List(v.Groups, static (g, i, _, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (i is not 0)
+ {
+ b.Append(" | ");
+ }
+
+ b.Append($"VariantGroupKinds.{g}");
+ })});");
+ },
+ VariantExpression: "Kind",
+ Default: static (_, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append("throw CreateInvalidKindException();");
+ })}}
+ }
+ }
+
+ private {{typeof(InvalidOperationException)}} CreateInvalidKindException()
+ => new($"The {nameof(VariantModel)} instance was not initialized correctly and is holding an invalid value: {Kind}");
+ """
+ );
+ });
+
+ var type = Create((Model, members), static (t, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var (model, members) = t;
+
+ b.Append($"""
+ {Summary(model, static (m, b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.Append($"Models and provides strongly typed access to the variants of {Cref(m.DocsCommentId)}.");
+ })}
+ {Type(
+ "public readonly record struct",
+ "VariantModel",
+ members)}
+ """);
+ });
+
+ var region = Region("VariantModel", type);
+
+ builder.Append(region);
+ }
+}
diff --git a/Janus.Analyzers/Components/VariantsSwitchComponent.cs b/Janus.Analyzers/Components/VariantsSwitchComponent.cs
new file mode 100644
index 0000000..95f0a84
--- /dev/null
+++ b/Janus.Analyzers/Components/VariantsSwitchComponent.cs
@@ -0,0 +1,60 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using Lyra;
+
+internal readonly record struct VariantsSwitchComponent(
+ UnionModel Model,
+ Action Append,
+ Action? Default = null,
+ String VariantExpression = "Variant.Kind") : ICSharpSourceComponent
+{
+ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ builder.AppendLine($"switch({VariantExpression})")
+ .AppendLine('{')
+ .Indent();
+
+ foreach (var variant in Model.Variants)
+ {
+ builder.AppendLine($"case VariantKind.{variant.Name}:")
+ .AppendLine('{')
+ .Indent();
+
+ Append.Invoke(variant, Model, builder, cancellationToken);
+
+ builder.AppendLine()
+ .Detent()
+ .AppendLine('}');
+ }
+
+ builder.AppendLine("default:")
+ .AppendLine('{')
+ .Indent();
+
+ if (Default is not null)
+ {
+ Default.Invoke(Model, builder, cancellationToken);
+ }
+ else
+ {
+ builder.Append("throw CreateUnknownVariantException();");
+ }
+
+ builder.AppendLine()
+ .Detent()
+ .AppendLine('}');
+
+ builder.Detent()
+ .Append('}');
+ }
+
+ public Boolean Equals(VariantsSwitchComponent other) =>
+ throw new NotSupportedException("Equals is not supported on this type.");
+
+ public override Int32 GetHashCode() =>
+ throw new NotSupportedException("GetHashCode is not supported on this type.");
+}
diff --git a/Janus.Analyzers/DiagnosticDescriptors.cs b/Janus.Analyzers/DiagnosticDescriptors.cs
new file mode 100644
index 0000000..daeed37
--- /dev/null
+++ b/Janus.Analyzers/DiagnosticDescriptors.cs
@@ -0,0 +1,166 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using Microsoft.CodeAnalysis;
+
+internal static class DiagnosticDescriptors
+{
+ public static DiagnosticDescriptor ToStringSettingIgnored { get; } = new(
+ id: "RMJ0001",
+ title: "`ToStringSetting` is ignored due to user defined `ToString` implementation",
+ messageFormat: "`{0}` is ignored due to user defined `ToString` implementation in `{1}`",
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ public static DiagnosticDescriptor UnionMayNotBeRecordType { get; } = new(
+ id: "RMJ0002",
+ title: "Union may not be record type",
+ messageFormat: "`{0}` may not be a record type",
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ public static DiagnosticDescriptor GenericUnionsCannotBeJsonSerializable { get; } = new(
+ id: "RMJ0003",
+ title: "Generic unions cannot be json serializable",
+ messageFormat: "`{0}` is a generic type and may therefore not be json serializable",
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ public static DiagnosticDescriptor NoMoreThan31VariantGroupsMayBeDefined { get; } = new(
+ id: "RMJ0004",
+ title: "No more than 31 variant groups may be defined",
+ messageFormat: "`{0}` defines {1} groups, but no more than 31 groups may be defined",
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ public static DiagnosticDescriptor UnionMayNotBeStatic { get; } = new(
+ id: "RMJ0005",
+ title: "Union may not be static",
+ messageFormat: "`{0}` may not be static",
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ public static DiagnosticDescriptor VariantNamesMustBeUnique { get; } = new(
+ id: "RMJ0006",
+ title: "Variant names must be unique",
+ messageFormat: "`{0}` already defines a variant with the name `{1}`",
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ public static DiagnosticDescriptor EnsureValidStructUnionState { get; } = new(
+ id: "RMJ0007",
+ title: "Ensure that struct unions have at least one struct or nullable reference type variant",
+ messageFormat:
+ "`{0}` is a struct union but does not have a struct or nullable reference type variant. In order to ensure the union is always in a correct state, a struct variant or nullable reference type should be provided. If no such variant is provided, the union should be a class",
+ category: "Design",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ public static DiagnosticDescriptor InterfaceVariantIsExcludedFromConversionOperators { get; } = new(
+ id: "RMJ0008",
+ title: "Interface variants are excluded from conversion operator generation",
+ messageFormat:
+ "Variant `{0}` of union `{1}` is excluded from conversion operator generation because it is an interface",
+ category: "Design",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ public static DiagnosticDescriptor VariantTypesMustBeUnique { get; } = new(
+ id: "RMJ0009",
+ title: "Variant types must be unique",
+ messageFormat: "`{0}` already defines a variant with the type `{1}`",
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ public static DiagnosticDescriptor ObjectCannotBeUsedAsAVariant { get; } = new(
+ id: "RMJ0010",
+ title: "`object` cannot be used as a variant",
+ messageFormat: "`object` cannot be used as a variant of `{0}`",
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ public static DiagnosticDescriptor ValueTypeCannotBeUsedAsAVariantOfStructUnion { get; } = new(
+ id: "RMJ0019",
+ title: "`ValueType` cannot be used as a variant of struct union",
+ messageFormat: "`ValueType` cannot be used as a variant of `{0}`",
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ public static DiagnosticDescriptor UnionCannotBeUsedAsVariantOfItself { get; } = new(
+ id: "RMJ0012",
+ title: "Union cannot be used as variant of itself",
+ messageFormat: "`{0}` cannot be used as a variant of itself",
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ public static DiagnosticDescriptor UnionCannotExplicitlyDefineBaseType { get; } = new(
+ id: "RMJ0013",
+ title: "Union cannot explicitly define base type",
+ messageFormat: "`{0}` cannot explicitly define base type `{1}`",
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ public static DiagnosticDescriptor PreferNullableStructOverIsNullable { get; } = new(
+ id: "RMJ0014",
+ title: "Prefer `Nullable` over `IsNullable = true`",
+ messageFormat:
+ "Prefer `Nullable<{0}>` over `{1}` for variant `{2}`, as the `IsNullable` option only affects nullable reference type variants",
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ public static DiagnosticDescriptor NullableVariantNotAllowedAlongWithNonNullableVariant { get; } = new(
+ id: "RMJ0015",
+ title: "`Nullable` and `T` variants are not allowed for the same type `T`",
+ messageFormat: "`{0}` cannot have both `Nullable<{1}>` and `{1}` variants",
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true
+ );
+
+ public static DiagnosticDescriptor UnionTypeSettingsAttributeIgnoredDueToMissingUnionTypeAttribute { get; } = new(
+ id: "RMJ0016",
+ title: "`UnionTypeSettingsAttribute` is ignored, as no `UnionTypeAttribute` has been applied",
+ messageFormat: "`UnionTypeSettingsAttribute` on `{0}` is ignored, as no `UnionTypeAttribute` has been applied",
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ public static DiagnosticDescriptor DuplicateVariantGroupNamesAreIgnored { get; } = new(
+ id: "RMJ0017",
+ title: "Duplicate variant group names are ignored",
+ messageFormat: "The variant group `{0}` has been specified",
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true
+ );
+
+ public static DiagnosticDescriptor ClassUnionsShouldBeSealed { get; } = new(
+ id: "RMJ0018",
+ title: "Class unions should be sealed",
+ messageFormat: "The union `{0}` should be sealed",
+ category: "Design",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true
+ );
+
+ public static DiagnosticDescriptor UnionCannotBeRefStruct { get; } = new(
+ id: "RMJ0020",
+ title: "Union cannot be ref struct",
+ messageFormat: "Union `{0}` cannot be a ref struct",
+ category: "Usage",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+}
diff --git a/Janus.Analyzers/DiagnosticIds.cs b/Janus.Analyzers/DiagnosticIds.cs
new file mode 100644
index 0000000..b292a67
--- /dev/null
+++ b/Janus.Analyzers/DiagnosticIds.cs
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+///
+/// Provides access to diagnostic ids.
+///
+public static class DiagnosticIds
+{
+#pragma warning disable CS1591
+ public const String ToStringSettingIgnored = "RMJ0001";
+ public const String UnionMayNotBeRecordType = "RMJ0002";
+ public const String GenericUnionsCannotBeJsonSerializable = "RMJ0003";
+ public const String NoMoreThan31VariantGroupsMayBeDefined = "RMJ0004";
+ public const String UnionMayNotBeStatic = "RMJ0005";
+ public const String VariantNamesMustBeUnique = "RMJ0006";
+ public const String EnsureValidStructUnionState = "RMJ0007";
+ public const String InterfaceVariantIsExcludedFromConversionOperators = "RMJ0008";
+ public const String VariantTypesMustBeUnique = "RMJ0009";
+ public const String ObjectCannotBeUsedAsAVariant = "RMJ0010";
+ public const String ValueTypeCannotBeUsedAsAVariantOfStructUnion = "RMJ0019";
+ public const String UnionCannotBeUsedAsVariantOfItself = "RMJ0012";
+ public const String UnionCannotExplicitlyDefineBaseType = "RMJ0013";
+ public const String PreferNullableStructOverIsNullable = "RMJ0014";
+ public const String NullableVariantNotAllowedAlongWithNonNullableVariant = "RMJ0015";
+ public const String UnionTypeSettingsAttributeIgnoredDueToMissingUnionTypeAttribute = "RMJ0016";
+ public const String DuplicateVariantGroupNamesAreIgnored = "RMJ0017";
+ public const String ClassUnionsShouldBeSealed = "RMJ0018";
+ public const String UnionCannotBeRefStruct = "RMJ0020";
+#pragma warning restore CS1591
+}
diff --git a/Janus.Analyzers/Janus.Analyzers.csproj b/Janus.Analyzers/Janus.Analyzers.csproj
new file mode 100644
index 0000000..4786796
--- /dev/null
+++ b/Janus.Analyzers/Janus.Analyzers.csproj
@@ -0,0 +1,40 @@
+
+
+
+ netstandard2.0
+ false
+ true
+ true
+ true
+ enable
+
+
+
+
+ $(DefineConstants);RHOMICRO_CODEANALYSIS_JANUS_ANALYZERS
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
diff --git a/Janus.Analyzers/JanusAnalyzer.AttributeContext.cs b/Janus.Analyzers/JanusAnalyzer.AttributeContext.cs
new file mode 100644
index 0000000..1a8e9f0
--- /dev/null
+++ b/Janus.Analyzers/JanusAnalyzer.AttributeContext.cs
@@ -0,0 +1,212 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Operations;
+
+partial class JanusAnalyzer
+{
+ readonly partial record struct AttributeAnalysisContext(
+ SemanticModel SemanticModel,
+ IObjectCreationOperation AttributeOperation,
+ AttributeSyntax AttributeSyntax,
+ INamedTypeSymbol AttributeSymbol,
+ ISymbol TargetSymbol)
+ {
+ public Boolean TryGetUnionTypeSymbol([NotNullWhen(true)] out INamedTypeSymbol? unionTypeSymbol)
+ {
+ switch (TargetSymbol)
+ {
+ case INamedTypeSymbol target:
+ unionTypeSymbol = target;
+ return true;
+ case ITypeParameterSymbol parameter:
+ unionTypeSymbol = parameter.ContainingType;
+ return true;
+ default:
+ unionTypeSymbol = null;
+ return false;
+ }
+ }
+
+ [TypeSymbolPattern(typeof(UnionTypeSettingsAttribute))]
+ private static partial Boolean IsUnionTypeSettingsAttribute(ITypeSymbol? type);
+
+ private static Boolean IsUnionTypeAttribute(ITypeSymbol? type)
+ {
+ var result = type is INamedTypeSymbol
+ {
+ Name: "UnionTypeAttribute",
+ TypeArguments.Length: < 9,
+ ContainingNamespace:
+ {
+ Name: "CodeAnalysis",
+ ContainingNamespace:
+ {
+ Name: "RhoMicro",
+ ContainingNamespace:
+ {
+ IsGlobalNamespace: true
+ }
+ }
+ }
+ };
+
+ return result;
+ }
+
+ public static Boolean TryCreateForUnionTypeSettingsAttribute(
+ OperationAnalysisContext ctx,
+ out AttributeAnalysisContext attributeAnalysisContext) =>
+ TryCreateForUnionTypeSettingsAttribute(
+ ctx.Operation,
+ GetContainingSymbol(ctx),
+ out attributeAnalysisContext,
+ ctx.CancellationToken);
+
+ public static Boolean TryCreateForUnionTypeSettingsAttribute(
+ IOperation operation,
+ ISymbol containingSymbol,
+ out AttributeAnalysisContext attributeAnalysisContext,
+ CancellationToken ct)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ attributeAnalysisContext = default;
+
+ if (operation is not IAttributeOperation
+ {
+ Operation: IObjectCreationOperation
+ {
+ Type: INamedTypeSymbol attributeSymbol
+ } attributeOperation
+ })
+ {
+ return false;
+ }
+
+ if (!IsUnionTypeSettingsAttribute(attributeSymbol))
+ {
+ return false;
+ }
+
+ if (attributeOperation.Syntax is not AttributeSyntax attributeSyntax)
+ {
+ return false;
+ }
+
+ if (attributeOperation.SemanticModel is not { } semanticModel)
+ {
+ return false;
+ }
+
+ attributeAnalysisContext = new AttributeAnalysisContext(
+ semanticModel,
+ attributeOperation,
+ attributeSyntax,
+ AttributeSymbol: attributeSymbol,
+ TargetSymbol: containingSymbol);
+
+ return true;
+ }
+
+ public static Boolean TryCreateForUnionTypeAttribute(
+ OperationAnalysisContext ctx,
+ out AttributeAnalysisContext attributeAnalysisContext) =>
+ TryCreateForUnionTypeAttribute(
+ ctx.Operation,
+ GetContainingSymbol(ctx),
+ out attributeAnalysisContext,
+ ctx.CancellationToken);
+
+ private static ISymbol GetContainingSymbol(OperationAnalysisContext ctx)
+ {
+ var ct = ctx.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (ctx is
+ not
+ {
+ Operation.Syntax.Parent.Parent: TypeParameterSyntax
+ {
+ Parent: TypeParameterListSyntax
+ {
+ Parameters: var typeParameterSyntaxes
+ }
+ } typeParameterSyntax,
+ ContainingSymbol: INamedTypeSymbol
+ {
+ TypeParameters: [_, ..] typeParameterSymbols
+ }
+ } || typeParameterSyntaxes.Count != typeParameterSymbols.Length)
+ {
+ return ctx.ContainingSymbol;
+ }
+
+ var index = 0;
+ for (; index < typeParameterSyntaxes.Count; index++)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (typeParameterSyntaxes[index].Equals(typeParameterSyntax))
+ {
+ return typeParameterSymbols[index];
+ }
+ }
+
+ return ctx.ContainingSymbol;
+ }
+
+ public static Boolean TryCreateForUnionTypeAttribute(
+ IOperation operation,
+ ISymbol containingSymbol,
+ out AttributeAnalysisContext attributeAnalysisContext,
+ CancellationToken ct)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ attributeAnalysisContext = default;
+
+ if (operation is not IAttributeOperation
+ {
+ Operation: IObjectCreationOperation
+ {
+ Type: INamedTypeSymbol attributeSymbol
+ } attributeOperation
+ })
+ {
+ return false;
+ }
+
+ if (!IsUnionTypeAttribute(attributeOperation.Type))
+ {
+ return false;
+ }
+
+ if (attributeOperation.Syntax is not AttributeSyntax attributeSyntax)
+ {
+ return false;
+ }
+
+ if (attributeOperation.SemanticModel is not { } semanticModel)
+ {
+ return false;
+ }
+
+ attributeAnalysisContext = new AttributeAnalysisContext(
+ semanticModel,
+ attributeOperation,
+ attributeSyntax,
+ AttributeSymbol: attributeSymbol,
+ TargetSymbol: containingSymbol);
+
+ return true;
+ }
+ }
+}
diff --git a/Janus.Analyzers/JanusAnalyzer.cs b/Janus.Analyzers/JanusAnalyzer.cs
new file mode 100644
index 0000000..88ad9a7
--- /dev/null
+++ b/Janus.Analyzers/JanusAnalyzer.cs
@@ -0,0 +1,1620 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.CodeAnalysis.Operations;
+
+///
+/// Generates diagnostics for guiding usage of the .
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed partial class JanusAnalyzer : DiagnosticAnalyzer
+{
+ ///
+ public override ImmutableArray SupportedDiagnostics { get; } =
+ [
+ DiagnosticDescriptors.ToStringSettingIgnored,
+ DiagnosticDescriptors.UnionMayNotBeRecordType,
+ DiagnosticDescriptors.GenericUnionsCannotBeJsonSerializable,
+ DiagnosticDescriptors.NoMoreThan31VariantGroupsMayBeDefined,
+ DiagnosticDescriptors.UnionMayNotBeStatic,
+ DiagnosticDescriptors.VariantNamesMustBeUnique,
+ DiagnosticDescriptors.EnsureValidStructUnionState,
+ DiagnosticDescriptors.InterfaceVariantIsExcludedFromConversionOperators,
+ DiagnosticDescriptors.VariantTypesMustBeUnique,
+ DiagnosticDescriptors.ObjectCannotBeUsedAsAVariant,
+ DiagnosticDescriptors.ValueTypeCannotBeUsedAsAVariantOfStructUnion,
+ DiagnosticDescriptors.UnionCannotExplicitlyDefineBaseType,
+ DiagnosticDescriptors.UnionCannotBeUsedAsVariantOfItself,
+ DiagnosticDescriptors.PreferNullableStructOverIsNullable,
+ DiagnosticDescriptors.NullableVariantNotAllowedAlongWithNonNullableVariant,
+ DiagnosticDescriptors.UnionTypeSettingsAttributeIgnoredDueToMissingUnionTypeAttribute,
+ DiagnosticDescriptors.DuplicateVariantGroupNamesAreIgnored,
+ DiagnosticDescriptors.ClassUnionsShouldBeSealed,
+ DiagnosticDescriptors.UnionCannotBeRefStruct,
+ ];
+
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ _ = context ?? throw new ArgumentNullException(nameof(context));
+
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+
+ context.RegisterOperationAction(
+ ReportToStringSettingIgnored, OperationKind.Attribute);
+ context.RegisterOperationAction(
+ ReportUnionMayNotBeRecordType, OperationKind.Attribute);
+ context.RegisterOperationAction(
+ ReportGenericUnionsCannotBeJsonSerializable, OperationKind.Attribute);
+ context.RegisterOperationAction(
+ ReportNoMoreThan31VariantGroupsMayBeDefined, OperationKind.Attribute);
+ context.RegisterOperationAction(
+ ReportUnionMayNotBeStatic, OperationKind.Attribute);
+ context.RegisterOperationAction(
+ ReportVariantNamesMustBeUnique, OperationKind.Attribute);
+ context.RegisterOperationAction(
+ ReportEnsureValidStructUnionState, OperationKind.Attribute);
+ context.RegisterOperationAction(
+ ReportInterfaceVariantIsExcludedFromConversionOperators, OperationKind.Attribute);
+ context.RegisterOperationAction(
+ ReportVariantTypesMustBeUnique, OperationKind.Attribute);
+ context.RegisterOperationAction(
+ ReportObjectCannotBeUsedAsAVariant, OperationKind.Attribute);
+ context.RegisterOperationAction(
+ ReportValueTypeCannotBeUsedAsAVariantOfStructUnion, OperationKind.Attribute);
+ context.RegisterSymbolAction(
+ ReportUnionCannotExplicitlyDefineBaseType, SymbolKind.NamedType);
+ context.RegisterOperationAction(
+ ReportUnionCannotBeUsedAsVariantOfItself, OperationKind.Attribute);
+ context.RegisterOperationAction(
+ ReportPreferNullableStructOverIsNullable, OperationKind.Attribute);
+ context.RegisterOperationAction(
+ ReportNullableVariantNotAllowedAlongWithNonNullableVariant, OperationKind.Attribute);
+ context.RegisterSymbolAction(
+ ReportUnionTypeSettingsAttributeIgnoredDueToMissingUnionTypeAttribute, SymbolKind.NamedType);
+ context.RegisterOperationAction(
+ ReportDuplicateVariantGroupNamesAreIgnored, OperationKind.Attribute);
+ context.RegisterSymbolAction(
+ ReportClassUnionsShouldBeSealed, SymbolKind.NamedType);
+ context.RegisterSymbolAction(
+ ReportUnionCannotBeRefStruct, SymbolKind.NamedType);
+ }
+
+ private static void ReportUnionCannotBeRefStruct(SymbolAnalysisContext ctx)
+ {
+ var ct = ctx.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (ctx.Symbol is not INamedTypeSymbol
+ {
+ IsRefLikeType: true
+ } target)
+ {
+ return;
+ }
+
+ var isUnion = false;
+ var attributes = target.GetAttributes();
+
+ foreach (var attribute in attributes)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (!attribute.IsUnionTypeAttribute())
+ {
+ continue;
+ }
+
+ isUnion = true;
+ break;
+ }
+
+ if (!isUnion)
+ {
+ return;
+ }
+
+ var unionName = target.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
+
+ foreach (var reference in target.DeclaringSyntaxReferences)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (reference.GetSyntax(ct) is not TypeDeclarationSyntax
+ {
+ Identifier: { } identifier
+ })
+ {
+ continue;
+ }
+
+ var location = identifier.GetLocation();
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.UnionCannotBeRefStruct,
+ location,
+ messageArgs:
+ [
+ unionName
+ ]);
+ ctx.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ private static void ReportClassUnionsShouldBeSealed(SymbolAnalysisContext ctx)
+ {
+ var ct = ctx.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (ctx.Symbol is not INamedTypeSymbol
+ {
+ IsSealed: false,
+ IsReferenceType: true
+ } target)
+ {
+ return;
+ }
+
+ var isUnion = false;
+
+ var attributes = target.GetAttributes();
+ foreach (var attribute in attributes)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (attribute.IsUnionTypeAttribute())
+ {
+ isUnion = true;
+ break;
+ }
+ }
+
+ if (!isUnion)
+ {
+ return;
+ }
+
+ var unionName = target.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
+
+ foreach (var reference in target.DeclaringSyntaxReferences)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (reference.GetSyntax(ct) is not TypeDeclarationSyntax
+ {
+ Identifier: { } identifier
+ })
+ {
+ continue;
+ }
+
+ var location = identifier.GetLocation();
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.ClassUnionsShouldBeSealed,
+ location,
+ messageArgs:
+ [
+ unionName
+ ]);
+ ctx.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ private static void ReportDuplicateVariantGroupNamesAreIgnored(OperationAnalysisContext ctx)
+ {
+ var ct = ctx.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (!AttributeAnalysisContext.TryCreateForUnionTypeAttribute(ctx, out var attributeContext))
+ {
+ return;
+ }
+
+ if (attributeContext is not
+ {
+ AttributeOperation.Initializer.Initializers: [_, ..] initializers,
+ AttributeSymbol:
+ {
+ TypeArguments: [_, ..]
+ }
+ })
+ {
+ return;
+ }
+
+ foreach (var initializer in initializers)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (initializer is not ISimpleAssignmentOperation
+ {
+ Target: IPropertyReferenceOperation
+ {
+ Member.Name: nameof(UnionTypeAttribute.Groups)
+ },
+ Value: { } value
+ })
+ {
+ continue;
+ }
+
+ var elements = value switch
+ {
+ IConversionOperation
+ {
+ Operand: ICollectionExpressionOperation
+ {
+ Elements: { } e
+ }
+ } => e,
+ IArrayCreationOperation
+ {
+ Initializer.ElementValues: { } e
+ } => e,
+ _ => []
+ };
+
+ if (elements is [] or [_])
+ {
+ continue;
+ }
+
+ var groups = new HashSet();
+
+ foreach (var element in elements)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (element.ConstantValue is not { HasValue: true, Value: String group })
+ {
+ continue;
+ }
+
+ if (groups.Add(group))
+ {
+ continue;
+ }
+
+ var location = element.Syntax.GetLocation();
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.DuplicateVariantGroupNamesAreIgnored,
+ location,
+ messageArgs:
+ [
+ group
+ ]);
+ ctx.ReportDiagnostic(diagnostic);
+ }
+ }
+ }
+
+ private static void ReportUnionTypeSettingsAttributeIgnoredDueToMissingUnionTypeAttribute(SymbolAnalysisContext ctx)
+ {
+ var ct = ctx.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (ctx.Symbol is not INamedTypeSymbol target)
+ {
+ return;
+ }
+
+ var attributes = target.GetAttributes();
+ var settingsLocation = Location.None;
+
+ foreach (var attribute in attributes)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (attribute.IsUnionTypeAttribute())
+ {
+ return;
+ }
+
+ if (attribute.IsUnionTypeSettingsAttribute())
+ {
+ settingsLocation = attribute.ApplicationSyntaxReference?
+ .GetSyntax(ct)
+ .GetLocation() ?? Location.None;
+ }
+ }
+
+ foreach (var typeParameter in target.TypeParameters)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var typeParameterAttributes = typeParameter.GetAttributes();
+
+ foreach (var attribute in typeParameterAttributes)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (attribute.IsUnionTypeAttribute())
+ {
+ return;
+ }
+ }
+ }
+
+ if (settingsLocation == Location.None)
+ {
+ return;
+ }
+
+ var unionName = target.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.UnionTypeSettingsAttributeIgnoredDueToMissingUnionTypeAttribute,
+ settingsLocation,
+ messageArgs:
+ [
+ unionName
+ ]);
+ ctx.ReportDiagnostic(diagnostic);
+ }
+
+ private static void ReportNullableVariantNotAllowedAlongWithNonNullableVariant(OperationAnalysisContext ctx)
+ {
+ var ct = ctx.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (!AttributeAnalysisContext.TryCreateForUnionTypeAttribute(ctx, out var attributeContext))
+ {
+ return;
+ }
+
+ if (!attributeContext.TryGetUnionTypeSymbol(out var target)
+ || attributeContext is not
+ {
+ AttributeSymbol.TypeArguments: [_, ..] localTypeArgumentSymbols,
+ AttributeSyntax.Name: GenericNameSyntax
+ {
+ TypeArgumentList.Arguments: [_, ..] localTypeArgumentSyntaxes
+ }
+ })
+ {
+ return;
+ }
+
+ if (localTypeArgumentSyntaxes.Count != localTypeArgumentSymbols.Length)
+ {
+ return;
+ }
+
+ var typeLocations = new Dictionary>(SymbolEqualityComparer.Default);
+
+ for (var i = 0; i < localTypeArgumentSymbols.Length; i++)
+ {
+ var typeArgumentSymbol = localTypeArgumentSymbols[i];
+ var location = localTypeArgumentSyntaxes[i].GetLocation();
+
+ if (typeArgumentSymbol is INamedTypeSymbol
+ {
+ OriginalDefinition:
+ {
+ SpecialType: SpecialType.System_Nullable_T
+ },
+ TypeArguments: [{ } actualTypeArgumentSymbol]
+ })
+ {
+ typeArgumentSymbol = actualTypeArgumentSymbol;
+ }
+
+ if (!typeLocations.TryGetValue(typeArgumentSymbol, out var locations))
+ {
+ typeLocations.Add(typeArgumentSymbol, locations = []);
+ }
+
+ locations.Add(location);
+ }
+
+ var attributes = target.GetAttributes();
+ var nullableTypeArguments = new HashSet(SymbolEqualityComparer.Default);
+ var nonNullableTypeArguments = new HashSet(SymbolEqualityComparer.Default);
+
+ foreach (var attribute in attributes)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (!attribute.IsUnionTypeAttribute())
+ {
+ continue;
+ }
+
+ if (attribute.AttributeClass is not
+ {
+ TypeArguments: [_, ..] globalTypeArgumentSymbols
+ })
+ {
+ continue;
+ }
+
+ foreach (var typeArgumentSymbol in globalTypeArgumentSymbols)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var nonNullableTypeArgumentSymbol = typeArgumentSymbol;
+ var isNullable = false;
+
+ if (typeArgumentSymbol is INamedTypeSymbol
+ {
+ OriginalDefinition:
+ {
+ SpecialType: SpecialType.System_Nullable_T
+ },
+ TypeArguments: [{ } actualTypeArgumentSymbol]
+ })
+ {
+ nonNullableTypeArgumentSymbol = actualTypeArgumentSymbol;
+ isNullable = true;
+ }
+
+ if (!typeLocations.ContainsKey(nonNullableTypeArgumentSymbol))
+ {
+ continue;
+ }
+
+ if (isNullable)
+ {
+ nullableTypeArguments.Add(nonNullableTypeArgumentSymbol);
+ }
+ else
+ {
+ nonNullableTypeArguments.Add(nonNullableTypeArgumentSymbol);
+ }
+ }
+ }
+
+ var typeName = target.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
+
+ foreach (var nullableTypeArgument in nullableTypeArguments)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (!nonNullableTypeArguments.Contains(nullableTypeArgument) ||
+ !typeLocations.TryGetValue(nullableTypeArgument, out var locations))
+ {
+ continue;
+ }
+
+ var variantName = nullableTypeArgument.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
+
+ foreach (var location in locations)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.NullableVariantNotAllowedAlongWithNonNullableVariant,
+ location,
+ messageArgs:
+ [
+ typeName,
+ variantName
+ ]);
+ ctx.ReportDiagnostic(diagnostic);
+ }
+ }
+ }
+
+ private static void ReportPreferNullableStructOverIsNullable(OperationAnalysisContext ctx)
+ {
+ var ct = ctx.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (!AttributeAnalysisContext.TryCreateForUnionTypeAttribute(ctx, out var attributeContext))
+ {
+ return;
+ }
+
+ if (attributeContext.AttributeSyntax.Name is not GenericNameSyntax
+ {
+ TypeArgumentList.Arguments: [{ } variantSyntax]
+ })
+ {
+ return;
+ }
+
+ if (attributeContext.SemanticModel.GetTypeInfo(variantSyntax).Type is not
+ {
+ IsValueType: true,
+ OriginalDefinition:
+ {
+ SpecialType: not SpecialType.System_Nullable_T
+ }
+ } variantSymbol)
+ {
+ return;
+ }
+
+ if (attributeContext.AttributeOperation.Initializer?.Initializers is not [_, ..] initializers)
+ {
+ return;
+ }
+
+ var variantTypeName = variantSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
+ var isNullableText = String.Empty;
+ var location = Location.None;
+ var variantName = variantSymbol.Name;
+
+ foreach (var initializer in initializers)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (initializer is not ISimpleAssignmentOperation
+ {
+ Target: IPropertyReferenceOperation
+ {
+ Member.Name: { } assignedMember
+ },
+ Value.ConstantValue: { HasValue: true, Value: { } assignedValue }
+ } assignmentOperation)
+ {
+ continue;
+ }
+
+ if (assignedMember is nameof(UnionTypeAttribute.Name))
+ {
+ if (assignedValue is String explicitName)
+ {
+ variantName = explicitName;
+ }
+
+ continue;
+ }
+
+ if (assignedMember is nameof(UnionTypeAttribute.IsNullable))
+ {
+ if (assignedValue is false)
+ {
+ return;
+ }
+
+ isNullableText = assignmentOperation.Syntax.ToString();
+ location = assignmentOperation.Syntax.GetLocation();
+ }
+ }
+
+ if (isNullableText is [])
+ {
+ return;
+ }
+
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.PreferNullableStructOverIsNullable,
+ location,
+ messageArgs:
+ [
+ variantTypeName,
+ isNullableText,
+ variantName
+ ]);
+ ctx.ReportDiagnostic(diagnostic);
+ }
+
+ private static void ReportUnionCannotBeUsedAsVariantOfItself(OperationAnalysisContext ctx)
+ {
+ var ct = ctx.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (!AttributeAnalysisContext.TryCreateForUnionTypeAttribute(ctx, out var attributeContext))
+ {
+ return;
+ }
+
+ if (attributeContext is not
+ {
+ AttributeSyntax.Name: GenericNameSyntax
+ {
+ TypeArgumentList.Arguments: [_, ..] typeArgumentSyntaxes
+ }
+ })
+ {
+ return;
+ }
+
+ for (var i = 0; i < typeArgumentSyntaxes.Count; i++)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var typeArgumentSyntax = typeArgumentSyntaxes[i];
+
+ if (attributeContext.SemanticModel.GetTypeInfo(typeArgumentSyntax, ct).Type is not { } variant)
+ {
+ continue;
+ }
+
+ if (!SymbolEqualityComparer.Default.Equals(variant, attributeContext.TargetSymbol))
+ {
+ continue;
+ }
+
+ var location = typeArgumentSyntaxes[i].GetLocation();
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.UnionCannotBeUsedAsVariantOfItself,
+ location,
+ messageArgs:
+ [
+ attributeContext.TargetSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)
+ ]);
+ ctx.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ private static void ReportUnionCannotExplicitlyDefineBaseType(SymbolAnalysisContext ctx)
+ {
+ var ct = ctx.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (ctx.Symbol is not INamedTypeSymbol
+ {
+ IsReferenceType: true,
+ BaseType:
+ {
+ SpecialType: not SpecialType.System_Object
+ } baseTypeSymbol
+ } target)
+ {
+ return;
+ }
+
+ var attributes = target.GetAttributes();
+ var baseTypeName = baseTypeSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
+ var isUnion = false;
+
+ foreach (var attribute in attributes)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (attribute.IsUnionTypeAttribute())
+ {
+ isUnion = true;
+ }
+ }
+
+ if (!isUnion)
+ {
+ return;
+ }
+
+ foreach (var reference in target.DeclaringSyntaxReferences)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (reference.GetSyntax(ct) is not TypeDeclarationSyntax
+ {
+ Identifier: { } identifier
+ })
+ {
+ continue;
+ }
+
+ var location = identifier.GetLocation();
+ var unionName = target.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.UnionCannotExplicitlyDefineBaseType,
+ location,
+ messageArgs:
+ [
+ unionName,
+ baseTypeName
+ ]);
+ ctx.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ private static void ReportValueTypeCannotBeUsedAsAVariantOfStructUnion(OperationAnalysisContext ctx)
+ {
+ var ct = ctx.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (!AttributeAnalysisContext.TryCreateForUnionTypeAttribute(ctx, out var attributeContext))
+ {
+ return;
+ }
+
+ if (!attributeContext.TryGetUnionTypeSymbol(out var target)
+ || !target.IsValueType
+ || attributeContext is not
+ {
+ AttributeSyntax.Name: GenericNameSyntax
+ {
+ TypeArgumentList.Arguments: [_, ..] typeArgumentSyntaxes
+ }
+ })
+ {
+ return;
+ }
+
+ for (var i = 0; i < typeArgumentSyntaxes.Count; i++)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var typeArgumentSyntax = typeArgumentSyntaxes[i];
+
+ if (attributeContext.SemanticModel.GetTypeInfo(typeArgumentSyntax, ct).Type is not
+ {
+ SpecialType: SpecialType.System_ValueType
+ })
+ {
+ continue;
+ }
+
+ var location = typeArgumentSyntaxes[i].GetLocation();
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.ValueTypeCannotBeUsedAsAVariantOfStructUnion,
+ location,
+ messageArgs:
+ [
+ attributeContext.TargetSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)
+ ]);
+ ctx.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ private static void ReportObjectCannotBeUsedAsAVariant(OperationAnalysisContext ctx)
+ {
+ var ct = ctx.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (!AttributeAnalysisContext.TryCreateForUnionTypeAttribute(ctx, out var attributeContext))
+ {
+ return;
+ }
+
+ if (attributeContext is not
+ {
+ AttributeSyntax.Name: GenericNameSyntax
+ {
+ TypeArgumentList.Arguments: [_, ..] typeArgumentSyntaxes
+ }
+ })
+ {
+ return;
+ }
+
+ for (var i = 0; i < typeArgumentSyntaxes.Count; i++)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var typeArgumentSyntax = typeArgumentSyntaxes[i];
+
+ if (attributeContext.SemanticModel.GetTypeInfo(typeArgumentSyntax, ct).Type is not
+ {
+ SpecialType: SpecialType.System_Object
+ })
+ {
+ continue;
+ }
+
+ var location = typeArgumentSyntaxes[i].GetLocation();
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.ObjectCannotBeUsedAsAVariant,
+ location,
+ messageArgs:
+ [
+ attributeContext.TargetSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)
+ ]);
+ ctx.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ private static void ReportVariantTypesMustBeUnique(OperationAnalysisContext ctx)
+ {
+ var ct = ctx.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (!AttributeAnalysisContext.TryCreateForUnionTypeAttribute(ctx, out var attributeContext))
+ {
+ return;
+ }
+
+ if (!attributeContext.TryGetUnionTypeSymbol(out var target)
+ || attributeContext is not
+ {
+ AttributeSymbol.TypeArguments: [_, ..] localTypeArgumentSymbols,
+ AttributeSyntax.Name: GenericNameSyntax
+ {
+ TypeArgumentList.Arguments: [_, ..] localTypeArgumentSyntaxes
+ }
+ })
+ {
+ return;
+ }
+
+ if (localTypeArgumentSyntaxes.Count != localTypeArgumentSymbols.Length)
+ {
+ return;
+ }
+
+ var typeLocations = new Dictionary>(SymbolEqualityComparer.Default);
+
+ for (var i = 0; i < localTypeArgumentSymbols.Length; i++)
+ {
+ var typeArgumentSymbol = localTypeArgumentSymbols[i];
+ var location = localTypeArgumentSyntaxes[i].GetLocation();
+
+ if (typeArgumentSymbol is INamedTypeSymbol
+ {
+ OriginalDefinition:
+ {
+ SpecialType: SpecialType.System_Nullable_T
+ },
+ TypeArguments: [{ } actualTypeArgumentSymbol]
+ })
+ {
+ typeArgumentSymbol = actualTypeArgumentSymbol;
+ }
+
+ if (!typeLocations.TryGetValue(typeArgumentSymbol, out var locations))
+ {
+ typeLocations.Add(typeArgumentSymbol, locations = []);
+ }
+
+ locations.Add(location);
+ }
+
+ var attributes = target.GetAttributes();
+ var duplicates = new Dictionary(SymbolEqualityComparer.Default);
+ foreach (var attribute in attributes)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (!attribute.IsUnionTypeAttribute())
+ {
+ continue;
+ }
+
+ if (attribute is not
+ {
+ AttributeClass.TypeArguments: [_, ..] typeArgumentSymbols
+ })
+ {
+ continue;
+ }
+
+ foreach (var typeArgumentSymbol in typeArgumentSymbols)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (!typeLocations.ContainsKey(typeArgumentSymbol))
+ {
+ continue;
+ }
+
+ if (duplicates.TryGetValue(typeArgumentSymbol, out var isDuplicate))
+ {
+ if (!isDuplicate)
+ {
+ duplicates[typeArgumentSymbol] = true;
+ }
+ }
+ else
+ {
+ duplicates[typeArgumentSymbol] = false;
+ }
+ }
+ }
+
+ if (duplicates.Count is 0)
+ {
+ return;
+ }
+
+ var unionName = target.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
+
+ foreach (var (variantType, isDuplicate) in duplicates)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (!isDuplicate)
+ {
+ continue;
+ }
+
+ if (!typeLocations.TryGetValue(variantType, out var locations))
+ {
+ continue;
+ }
+
+ foreach (var location in locations)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var variantTypeName = variantType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.VariantTypesMustBeUnique,
+ location,
+ messageArgs:
+ [
+ unionName,
+ variantTypeName
+ ]);
+ ctx.ReportDiagnostic(diagnostic);
+ }
+ }
+ }
+
+ private static void ReportInterfaceVariantIsExcludedFromConversionOperators(OperationAnalysisContext ctx)
+ {
+ var ct = ctx.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (!AttributeAnalysisContext.TryCreateForUnionTypeAttribute(ctx, out var attributeContext))
+ {
+ return;
+ }
+
+ if (attributeContext.AttributeSyntax.Name is not GenericNameSyntax
+ {
+ TypeArgumentList.Arguments: { } argumentSyntaxes
+ })
+ {
+ return;
+ }
+
+ var interfaceVariants = new Dictionary>();
+ for (var i = 0; i < argumentSyntaxes.Count; i++)
+ {
+ var typeArgumentSyntax = argumentSyntaxes[i];
+
+ if (attributeContext.SemanticModel.GetTypeInfo(typeArgumentSyntax, ct).Type is not INamedTypeSymbol
+ {
+ TypeKind: TypeKind.Interface
+ } typeArgumentSymbol)
+ {
+ continue;
+ }
+
+ var name = typeArgumentSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
+ var location = typeArgumentSyntax.GetLocation();
+
+ if (!interfaceVariants.TryGetValue(name, out var locations))
+ {
+ interfaceVariants.Add(name, locations = []);
+ }
+
+ locations.Add(location);
+ }
+
+ if (interfaceVariants.Count is 0)
+ {
+ return;
+ }
+
+ foreach (var (name, locations) in interfaceVariants)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ foreach (var location in locations)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.InterfaceVariantIsExcludedFromConversionOperators,
+ location,
+ messageArgs:
+ [
+ name,
+ attributeContext.TargetSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)
+ ]);
+ ctx.ReportDiagnostic(diagnostic);
+ }
+ }
+ }
+
+ private static void ReportEnsureValidStructUnionState(OperationAnalysisContext ctx)
+ {
+ var ct = ctx.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (!AttributeAnalysisContext.TryCreateForUnionTypeAttribute(ctx, out var attributeContext))
+ {
+ return;
+ }
+
+ if (!attributeContext.TryGetUnionTypeSymbol(out var target)
+ || target.TypeKind is TypeKind.Class)
+ {
+ return;
+ }
+
+ var model = UnionModel.Create(target, ct);
+
+ if (model.Variants.Any(v => v.Type is not { Kind: VariantTypeKind.Reference, IsNullable: false }))
+ {
+ return;
+ }
+
+ var locations = attributeContext
+ .TargetSymbol
+ .DeclaringSyntaxReferences
+ .Select(r => r.GetSyntax(ct))
+ .OfType()
+ .Select(d => d.Identifier.GetLocation());
+
+ foreach (var location in locations)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.EnsureValidStructUnionState,
+ location,
+ messageArgs:
+ [
+ attributeContext.TargetSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)
+ ]);
+ ctx.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ private static void ReportVariantNamesMustBeUnique(OperationAnalysisContext ctx)
+ {
+ var ct = ctx.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (!AttributeAnalysisContext.TryCreateForUnionTypeAttribute(ctx, out var attributeContext))
+ {
+ return;
+ }
+
+ var localNameLocations = new Dictionary>(StringComparer.Ordinal);
+ var duplicates = new Dictionary();
+
+ if (!tryGetNamedTypeTargetNames(out var unionType) && !tryGetTypeParameterTargetNames(out unionType))
+ {
+ return;
+ }
+
+ getGlobalNames(unionType);
+
+ var unionTypeName = unionType.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat);
+
+ foreach (var (variantName, isDuplicate) in duplicates)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (!isDuplicate)
+ {
+ continue;
+ }
+
+ if (!localNameLocations.TryGetValue(variantName, out var locations))
+ {
+ continue;
+ }
+
+ foreach (var location in locations)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.VariantNamesMustBeUnique,
+ location,
+ messageArgs:
+ [
+ unionTypeName,
+ variantName
+ ]);
+ ctx.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ Boolean tryGetTypeParameterTargetNames([NotNullWhen(true)] out INamedTypeSymbol? containingUnionType)
+ {
+ if (attributeContext is not
+ {
+ TargetSymbol: ITypeParameterSymbol
+ {
+ ContainingType: var containingType
+ } target
+ })
+ {
+ containingUnionType = null;
+ return false;
+ }
+
+ var hasExplicitName = false;
+ var initializers = attributeContext.AttributeOperation.Initializer?.Initializers ?? [];
+ foreach (var initializer in initializers)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (initializer is not ISimpleAssignmentOperation
+ {
+ Target: IMemberReferenceOperation
+ {
+ Member.Name: nameof(UnionTypeAttribute.Name)
+ },
+ Value.ConstantValue: { HasValue: true, Value: String explicitName }
+ } nameAssignmentOperation)
+ {
+ continue;
+ }
+
+ var location = nameAssignmentOperation.Syntax.GetLocation();
+
+ registerLocation(explicitName, location);
+ }
+
+ if (hasExplicitName)
+ {
+ }
+ else
+ {
+ var name = target.Name;
+ foreach (var reference in target.DeclaringSyntaxReferences)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (reference.GetSyntax(ct) is not TypeParameterSyntax
+ {
+ Identifier: var targetSyntaxIdentifier
+ })
+ {
+ continue;
+ }
+
+ var location = targetSyntaxIdentifier.GetLocation();
+ registerLocation(name, location);
+ }
+ }
+
+ containingUnionType = containingType;
+ return true;
+ }
+
+ Boolean tryGetNamedTypeTargetNames([NotNullWhen(true)] out INamedTypeSymbol? targetUnionType)
+ {
+ if (!attributeContext.TryGetUnionTypeSymbol(out var target)
+ || attributeContext is not
+ {
+ AttributeSymbol.TypeArguments: [_, ..] localTypeArgumentSymbols,
+ AttributeSyntax.Name: GenericNameSyntax
+ {
+ TypeArgumentList.Arguments: [_, ..] localTypeArgumentSyntaxes
+ }
+ } || localTypeArgumentSyntaxes.Count != localTypeArgumentSymbols.Length)
+ {
+ targetUnionType = null;
+ return false;
+ }
+
+ var hasExplicitName = false;
+ var initializers = attributeContext.AttributeOperation.Initializer?.Initializers ?? [];
+ foreach (var initializer in initializers)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (initializer is not ISimpleAssignmentOperation
+ {
+ Target: IMemberReferenceOperation
+ {
+ Member.Name: nameof(UnionTypeAttribute.Name)
+ },
+ Value.ConstantValue: { HasValue: true, Value: String explicitName }
+ } nameAssignmentOperation)
+ {
+ continue;
+ }
+
+ var location = nameAssignmentOperation.Syntax.GetLocation();
+
+ registerLocation(explicitName, location);
+ }
+
+ if (!hasExplicitName)
+ {
+ for (var i = 0; i < localTypeArgumentSymbols.Length; i++)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var typeArgumentSymbol = localTypeArgumentSymbols[i];
+ var variantName = typeArgumentSymbol.Name;
+ var location = localTypeArgumentSyntaxes[i].GetLocation();
+ registerLocation(variantName, location);
+ }
+ }
+
+ targetUnionType = target;
+ return true;
+ }
+
+ void registerName(String name)
+ {
+ if (!localNameLocations.ContainsKey(name))
+ {
+ return;
+ }
+
+ if (duplicates.TryGetValue(name, out var isDuplicate))
+ {
+ if (!isDuplicate)
+ {
+ duplicates[name] = true;
+ }
+ }
+ else
+ {
+ duplicates[name] = false;
+ }
+ }
+
+ void getGlobalNames(INamedTypeSymbol target)
+ {
+ var attributes = target.GetAttributes();
+
+ foreach (var attribute in attributes)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var typeArguments = attribute.AttributeClass?.TypeArguments ?? [];
+
+ foreach (var typeArgument in typeArguments)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (!attribute.TryGetUnionTypeAttributeModel(
+ new UnionTypeAttribute.Model.TypeArgumentState(typeArgument),
+ out var model))
+ {
+ continue;
+ }
+
+ if (model.Name is [_, ..] name)
+ {
+ registerName(name);
+ }
+ }
+ }
+
+ foreach (var typeParameter in target.TypeParameters)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var typeParameterAttributes = typeParameter.GetAttributes();
+
+ foreach (var attribute in typeParameterAttributes)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (!attribute.TryGetUnionTypeAttributeModel(
+ new UnionTypeAttribute.Model.TypeParameterState(typeParameter),
+ out var model))
+ {
+ continue;
+ }
+
+ var name = model.Name is [_, ..] explicitName
+ ? explicitName
+ : typeParameter.Name;
+
+ registerName(name);
+ }
+ }
+ }
+
+ void registerLocation(String name, Location location)
+ {
+ if (!localNameLocations.TryGetValue(name, out var locations))
+ {
+ localNameLocations.Add(name, locations = []);
+ }
+
+ locations.Add(location);
+ }
+ }
+
+ private static void ReportUnionMayNotBeStatic(OperationAnalysisContext ctx)
+ {
+ var ct = ctx.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (!AttributeAnalysisContext.TryCreateForUnionTypeAttribute(ctx, out var attributeContext))
+ {
+ return;
+ }
+
+ var locations = attributeContext
+ .TargetSymbol
+ .DeclaringSyntaxReferences
+ .Select(r => r.GetSyntax(ct))
+ .OfType()
+ .Select(d => d
+ .Modifiers
+ .FirstOrDefault(m => m.IsKind(SyntaxKind.StaticKeyword)))
+ .Where(m => m.IsKind(SyntaxKind.StaticKeyword))
+ .Select(m => m.GetLocation());
+
+ foreach (var location in locations)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.UnionMayNotBeStatic,
+ location,
+ messageArgs:
+ [
+ attributeContext.TargetSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)
+ ]);
+ ctx.ReportDiagnostic(diagnostic);
+ }
+ }
+
+ private static void ReportNoMoreThan31VariantGroupsMayBeDefined(OperationAnalysisContext ctx)
+ {
+ var ct = ctx.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (!AttributeAnalysisContext.TryCreateForUnionTypeAttribute(ctx, out var attributeContext))
+ {
+ return;
+ }
+
+ var groupDatum = attributeContext
+ .AttributeOperation
+ .Initializer?
+ .Initializers
+ .OfType()
+ .Select(i =>
+ {
+ if (i is not { Target: IPropertyReferenceOperation { Member.Name: nameof(UnionTypeAttribute.Groups) } })
+ {
+ return (groupsOperation: i, groupsCount: -1);
+ }
+
+ if (i.Value is
+ IConversionOperation
+ {
+ Operand:
+ ICollectionExpressionOperation
+ {
+ Elements.Length: > 31 and var collectionExpressionElementCount
+ }
+ })
+ {
+ return (groupsOperation: i, groupsCount: collectionExpressionElementCount);
+ }
+
+ if (i.Value is
+ IArrayCreationOperation
+ {
+ Initializer.ElementValues.Length: > 31 and var arrayInitializerElementCount
+ })
+ {
+ return (groupsOperation: i, groupsCount: arrayInitializerElementCount);
+ }
+
+ return (groupsOperation: i, groupsCount: -1);
+ })
+ .FirstOrDefault(t => t.groupsCount is not -1);
+
+ var (groupsOperation, groupsCount) = groupDatum.GetValueOrDefault();
+
+ if (groupsOperation is null)
+ {
+ return;
+ }
+
+ var location = groupsOperation.Syntax.GetLocation();
+
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.NoMoreThan31VariantGroupsMayBeDefined,
+ location,
+ messageArgs:
+ [
+ attributeContext.TargetSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat),
+ groupsCount
+ ]);
+
+ ctx.ReportDiagnostic(diagnostic);
+ }
+
+ private static void ReportGenericUnionsCannotBeJsonSerializable(OperationAnalysisContext ctx)
+ {
+ var ct = ctx.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (!AttributeAnalysisContext.TryCreateForUnionTypeSettingsAttribute(ctx, out var attributeContext))
+ {
+ return;
+ }
+
+ if (!attributeContext.TryGetUnionTypeSymbol(out var taget) || !taget.IsGenericType)
+ {
+ return;
+ }
+
+ var isJsonSerializableOperation = attributeContext
+ .AttributeOperation
+ .Initializer?
+ .Initializers
+ .OfType()
+ .FirstOrDefault(i => i is
+ {
+ Target: IPropertyReferenceOperation
+ {
+ Member.Name: nameof(UnionTypeSettingsAttribute.JsonConverterSetting)
+ },
+ Value.ConstantValue:
+ {
+ HasValue: true,
+ Value: (Int32)JsonConverterSetting.EmitJsonConverter
+ }
+ });
+
+ if (isJsonSerializableOperation is null)
+ {
+ return;
+ }
+
+ var location = isJsonSerializableOperation.Syntax.GetLocation();
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.GenericUnionsCannotBeJsonSerializable,
+ location,
+ messageArgs:
+ [
+ attributeContext.TargetSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)
+ ]);
+ ctx.ReportDiagnostic(diagnostic);
+ }
+
+ [TypeSymbolPattern(typeof(ToStringSetting))]
+ private static partial Boolean IsToStringSetting(ITypeSymbol? type);
+
+ private static void ReportToStringSettingIgnored(OperationAnalysisContext ctx)
+ {
+ var ct = ctx.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (!AttributeAnalysisContext.TryCreateForUnionTypeSettingsAttribute(ctx, out var attributeContext))
+ {
+ return;
+ }
+
+ var toStringSettingOperation = attributeContext
+ .AttributeOperation
+ .Initializer?
+ .Initializers
+ .FirstOrDefault(i => IsToStringSetting(i.Type) && i is not ISimpleAssignmentOperation
+ {
+ Value.ConstantValue: { HasValue: true, Value: ToStringSetting.None }
+ });
+
+ if (toStringSettingOperation is null)
+ {
+ return;
+ }
+
+ if (!attributeContext.TryGetUnionTypeSymbol(out var target))
+ {
+ return;
+ }
+
+ var hasNonGeneratedToString = false;
+
+ foreach (var member in target.GetMembers(nameof(ToString)))
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (member is not IMethodSymbol
+ {
+ Parameters: [],
+ IsOverride: true,
+ } method)
+ {
+ return;
+ }
+
+ foreach (var reference in method.DeclaringSyntaxReferences)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var declaration = reference.GetSyntax(ct);
+
+ if (declaration.SyntaxTree.FilePath.EndsWith(".g.cs"))
+ {
+ continue;
+ }
+
+ var text = declaration.SyntaxTree.GetText(ct);
+
+ if (text.Lines is not [{ } firstLine, ..])
+ {
+ continue;
+ }
+
+ if (text.GetSubText(firstLine.Span).ToString().StartsWith("// "))
+ {
+ continue;
+ }
+
+ hasNonGeneratedToString = true;
+ break;
+ }
+
+ if (hasNonGeneratedToString)
+ {
+ break;
+ }
+ }
+
+ if (!hasNonGeneratedToString)
+ {
+ return;
+ }
+
+ var location = toStringSettingOperation.Syntax.GetLocation();
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.ToStringSettingIgnored,
+ location,
+ messageArgs:
+ [
+ toStringSettingOperation.Syntax.ToString(),
+ attributeContext.TargetSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)
+ ]);
+ ctx.ReportDiagnostic(diagnostic);
+ }
+
+ private static void ReportUnionMayNotBeRecordType(OperationAnalysisContext ctx)
+ {
+ var ct = ctx.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (!AttributeAnalysisContext.TryCreateForUnionTypeAttribute(ctx, out var attributeContext))
+ {
+ return;
+ }
+
+ if (!attributeContext.TryGetUnionTypeSymbol(out var target) || !target.IsRecord)
+ {
+ return;
+ }
+
+ var locations = target
+ .DeclaringSyntaxReferences
+ .Select(r => r.GetSyntax(ct))
+ .OfType()
+ .Select(d => d.Keyword.GetLocation());
+
+ foreach (var location in locations)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var diagnostic = Diagnostic.Create(
+ DiagnosticDescriptors.UnionMayNotBeRecordType,
+ location,
+ messageArgs:
+ [
+ attributeContext.TargetSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)
+ ]);
+ ctx.ReportDiagnostic(diagnostic);
+ }
+ }
+}
diff --git a/Janus.Analyzers/JanusGenerator.cs b/Janus.Analyzers/JanusGenerator.cs
new file mode 100644
index 0000000..c75b8a4
--- /dev/null
+++ b/Janus.Analyzers/JanusGenerator.cs
@@ -0,0 +1,212 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Runtime.InteropServices;
+using Generated;
+using Library.Models;
+using Library.Models.Collections;
+using Lyra;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+///
+/// Generates union type implementations.
+///
+[Generator(LanguageNames.CSharp)]
+public sealed class JanusGenerator : IIncrementalGenerator
+{
+ private static readonly ImmutableArray _genericUnionTypeAttributeMetadataNames =
+ [
+ typeof(UnionTypeAttribute<>).FullName,
+ typeof(UnionTypeAttribute<,>).FullName,
+ typeof(UnionTypeAttribute<,,>).FullName,
+ typeof(UnionTypeAttribute<,,,>).FullName,
+ typeof(UnionTypeAttribute<,,,,>).FullName,
+ typeof(UnionTypeAttribute<,,,,,>).FullName,
+ typeof(UnionTypeAttribute<,,,,,,>).FullName,
+ typeof(UnionTypeAttribute<,,,,,,,>).FullName
+ ];
+
+ private static readonly CSharpSourceBuilderOptions _sourceBuilderOptions = new()
+ {
+ Prelude = static (b, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ b.AppendLine(
+ $"""
+ //
+ // This file was generated using the Janus source generator.
+ // SPDX-License-Identifier: MPL-2.0
+ //
+ #nullable enable
+ """);
+ }
+ };
+
+ ///
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var defaultSettingsProvider = context.SyntaxProvider
+ .ForAttributeWithMetadataName(
+ typeof(UnionTypeSettingsAttribute).FullName!,
+ static (n, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var result = n is CompilationUnitSyntax;
+
+ return result;
+ }, static (ctx, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (!ctx.Attributes[0]
+ .TryGetUnionTypeSettingsAttributeModel(out var settings, cancellationToken: ct))
+ {
+ return null;
+ }
+
+ return settings;
+ })
+ .Collect()
+ .Select(static (s, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (s is not [{ } settings, ..])
+ {
+ return UnionTypeSettingsAttribute.Model.InheritanceRoot;
+ }
+
+ // settings are copied in collection expression above,
+ // so we don't need to do that before mutating to
+ // avoid cache corruption
+ settings.Inherit(UnionTypeSettingsAttribute.Model.InheritanceRoot);
+ return settings;
+ });
+
+ var aggregateUnionTypesProvider = context.SyntaxProvider.ForAttributeWithMetadataName(
+ typeof(UnionTypeAttribute).FullName!,
+ static (n, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var result = n is TypeParameterSyntax;
+
+ return result;
+ }, static (ctx, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var result = ctx.TargetSymbol is ITypeParameterSymbol { ContainingType: { } target } &&
+ UnionModel.TryCreate(target, out var r, ct)
+ ? r
+ : null;
+
+ return result;
+ })
+ .Where(static m => m is not null)!
+ .Collect();
+
+ for (var i = 0; i < 8; i++)
+ {
+ var provider = context.SyntaxProvider.ForAttributeWithMetadataName(
+ _genericUnionTypeAttributeMetadataNames[i],
+ static (n, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var result = n is TypeDeclarationSyntax;
+
+ return result;
+ }, static (ctx, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var result = ctx.TargetSymbol is INamedTypeSymbol target &&
+ UnionModel.TryCreate(target, out var r, ct)
+ ? r
+ : null;
+
+ return result;
+ })
+ .Where(static m => m is not null)!
+ .Collect();
+
+ aggregateUnionTypesProvider = aggregateUnionTypesProvider
+ .Combine(provider)
+ .Select(static (t, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var (left, right) = t;
+
+ ImmutableArray result = [..left, ..right];
+
+ return result;
+ })
+ .WithComparer(ImmutableArrayEqualityComparer.Default);
+ }
+
+ var sourceProvider = aggregateUnionTypesProvider
+ .SelectMany(static (a, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var included = new HashSet();
+
+ foreach (var model in a)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ _ = included.Add(model);
+ }
+
+ return included;
+ })
+ .Combine(defaultSettingsProvider)
+ .Select(static (t, ct) =>
+ {
+ var (union, defaultSettings) = t;
+
+ // copy settings before mutating to avoid cache corruption
+ var unionSettings = union.Settings;
+ unionSettings.Inherit(defaultSettings);
+ union = union with { Settings = unionSettings };
+
+ var result = union.GetValidation(ct).HasUnsupportedSettingsAwareState
+ ? null
+ : union;
+
+ return result;
+ })!
+ .Where(m => m is not null)
+ .Select(static (m, ct) =>
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var sourceBuilder = new CSharpSourceBuilder(_sourceBuilderOptions);
+ var source = sourceBuilder
+ .SetCancellationToken(ct)
+ .Append(new UnionComponent(m))
+ .ToString();
+
+ var hintName = sourceBuilder
+ .Clear()
+ .SkipPreludeChecks()
+ .Append(new UnionTypeMetadataNameComponent(m))
+ .Append(".g.cs")
+ .ToString();
+
+ return (hintName, source);
+ });
+
+ context.RegisterSourceOutput(sourceProvider, static (ctx, t) => ctx.AddSource(t.hintName, t.source));
+ IncludedFileSources.RegisterToContext(context);
+ }
+}
diff --git a/Janus.Analyzers/Models/ContainingTypeModel.cs b/Janus.Analyzers/Models/ContainingTypeModel.cs
new file mode 100644
index 0000000..cb18008
--- /dev/null
+++ b/Janus.Analyzers/Models/ContainingTypeModel.cs
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using System.Collections.Immutable;
+using Library.Models.Collections;
+
+internal readonly record struct ContainingTypeModel(
+ String Modifier,
+ String Name,
+ EquatableList TypeParameters);
diff --git a/Janus.Analyzers/Models/TypeNames.cs b/Janus.Analyzers/Models/TypeNames.cs
new file mode 100644
index 0000000..9032145
--- /dev/null
+++ b/Janus.Analyzers/Models/TypeNames.cs
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using Lyra;
+
+internal record TypeNames
+{
+ public TypeNames(UnionModel model)
+ {
+ IUnionFactory = ComponentFactory.TypeName($"global::RhoMicro.CodeAnalysis.IUnionFactory<{new UnionTypeNameComponent(model)}>");
+ }
+
+ public TypeNameComponent IUnion { get; } = ComponentFactory.TypeName("global::RhoMicro.CodeAnalysis.IUnion");
+ public TypeNameComponent IUnionFactory { get; }
+}
diff --git a/Janus.Analyzers/Models/UnionModel.cs b/Janus.Analyzers/Models/UnionModel.cs
new file mode 100644
index 0000000..f8044ba
--- /dev/null
+++ b/Janus.Analyzers/Models/UnionModel.cs
@@ -0,0 +1,373 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+using Library.Models;
+using Library.Models.Collections;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+internal sealed partial record UnionModel(
+ UnionTypeKind TypeKind,
+ String Name,
+ String Namespace,
+ EquatableList ContainingTypes,
+ EquatableList Variants,
+ EquatableList VariantGroups,
+ EquatableList TypeParameters,
+ UnionTypeSettingsAttribute.Model Settings,
+ Boolean IsToStringUserProvided,
+ Boolean IsEqualsUserProvided,
+ Boolean IsGetHashCodeUserProvided,
+ Boolean AreEqualityOperatorsUserProvided,
+ String DocsCommentId,
+ Boolean EmitDocsComment)
+{
+ [field: MaybeNull] public TypeNames TypeNames => field ??= new TypeNames(this);
+
+ private static readonly SymbolDisplayFormat _namespaceFormat = new(
+ globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Omitted,
+ typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces);
+
+ private static Boolean HasUnsupportedStateSettingsUnaware(INamedTypeSymbol target, CancellationToken ct)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ // type keywords would be incorrectly emitted
+ if (target.IsRecord)
+ {
+ return true;
+ }
+
+ // partial class would not be emitted as static
+ if (target.IsStatic)
+ {
+ return true;
+ }
+
+ // ref structs introduce (for now) unsupported complexities
+ if (target.IsRefLikeType)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ public UnionModelValidation GetValidation(CancellationToken ct) => new(this, ct);
+
+ public static Boolean TryCreate(
+ INamedTypeSymbol target,
+ [NotNullWhen(true)] out UnionModel? model,
+ CancellationToken ct)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (HasUnsupportedStateSettingsUnaware(target, ct))
+ {
+ model = null;
+ return false;
+ }
+
+ model = Create(target, ct);
+
+ if (model.GetValidation(ct).HasUnsupportedSettingsUnawareStatePostCondition(target))
+ {
+ model = null;
+ return false;
+ }
+
+ return true;
+ }
+
+ public static UnionModel Create(
+ INamedTypeSymbol target,
+ CancellationToken ct)
+ {
+ using var ctx = ModelCreationContext.CreateDefault(ct);
+
+ var docsCommentId = target.GetDocumentationCommentId() ?? String.Empty;
+
+ var variantsList = new List();
+ var variantGroupsList = new List();
+ var typeParameters = ctx.CollectionFactory.CreateList();
+ ParseTargetAttributes(
+ target,
+ variantsList,
+ variantGroupsList,
+ out var settings,
+ ct);
+ ParseTargetTypeParametersAttributes(
+ target: target,
+ variants: variantsList,
+ variantGroups: variantGroupsList,
+ typeParameters: typeParameters, ct: ct);
+
+ var variants = SortVariants(variantsList, ctx);
+ var variantGroups = SortVariantGroups(variantGroupsList, ctx);
+
+ var typeKind = target.TypeKind is Microsoft.CodeAnalysis.TypeKind.Struct
+ ? UnionTypeKind.Struct
+ : UnionTypeKind.Class;
+ var containingTypes = ctx.CollectionFactory.CreateList();
+ if (target.ContainingType is { } t)
+ {
+ AppendContainingType(t, containingTypes, in ctx);
+ }
+
+ var (isToStringUserProvided,
+ isEqualsUserProvided,
+ isGetHashCodeUserProvided,
+ areEqualityOperatorsUserProvided) = (false, false, false, false);
+ var members = target.GetMembers();
+
+ foreach (var member in members)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ switch (member.Name)
+ {
+ case nameof(ToString):
+ if (member is IMethodSymbol { Parameters: [] })
+ {
+ isToStringUserProvided = true;
+ }
+
+ break;
+ case nameof(Equals):
+ if (member is IMethodSymbol { Parameters: [{ } singleParameter] }
+ && SymbolEqualityComparer.Default.Equals(singleParameter.Type, target))
+ {
+ isEqualsUserProvided = true;
+ }
+
+ break;
+ case nameof(GetHashCode):
+ if (member is IMethodSymbol { Parameters: [] })
+ {
+ isGetHashCodeUserProvided = true;
+ }
+
+ break;
+ case "op_Equality" or "op_Inequality":
+ if (member is IMethodSymbol { Parameters: [{ } firstParameter, { } secondParameter] }
+ && SymbolEqualityComparer.Default.Equals(firstParameter.Type, target)
+ && SymbolEqualityComparer.Default.Equals(secondParameter.Type, target))
+ {
+ areEqualityOperatorsUserProvided = true;
+ }
+
+ break;
+ }
+ }
+
+ var emitDocsComment = target.GetDocumentationCommentXml(cancellationToken: ct) is null or [];
+
+ var result = new UnionModel(
+ TypeKind: typeKind,
+ Name: target.Name,
+ Namespace: target.ContainingNamespace.ToDisplayString(_namespaceFormat),
+ ContainingTypes: containingTypes,
+ Variants: variants,
+ VariantGroups: variantGroups,
+ TypeParameters: typeParameters,
+ Settings: settings,
+ IsToStringUserProvided: isToStringUserProvided,
+ IsEqualsUserProvided: isEqualsUserProvided,
+ IsGetHashCodeUserProvided: isGetHashCodeUserProvided,
+ AreEqualityOperatorsUserProvided: areEqualityOperatorsUserProvided,
+ DocsCommentId: docsCommentId,
+ EmitDocsComment: emitDocsComment
+ );
+
+ return result;
+ }
+
+ private static EquatableList SortVariantGroups(
+ List variantGroupsList,
+ in ModelCreationContext ctx)
+ {
+ ctx.ThrowIfCancellationRequested();
+
+ variantGroupsList.Sort((x, y) => x.CompareTo(y, StringComparison.Ordinal));
+ variantGroupsList.Insert(0, "None");
+ var variantGroups = ctx.CollectionFactory.CreateList();
+ foreach (var variantGroup in variantGroupsList)
+ {
+ ctx.ThrowIfCancellationRequested();
+
+ variantGroups.Add(variantGroup);
+ }
+
+ return variantGroups;
+ }
+
+ private static EquatableList SortVariants(
+ List variantsList,
+ in ModelCreationContext ctx)
+ {
+ ctx.ThrowIfCancellationRequested();
+
+ variantsList.Sort((x, y) =>
+ {
+ var xTypePrecedence = getTypePrecedence(x.Type);
+ var yTypePrecedence = getTypePrecedence(y.Type);
+ // negative order means x precedes y in sort order, x has higher precedence than y
+ // positive order means y precedes x in sort order, y has higher precedence than x
+ var typeOrder = yTypePrecedence - xTypePrecedence;
+
+ var result = typeOrder == 0
+ ? x.Name.CompareTo(y.Name, StringComparison.Ordinal)
+ : typeOrder;
+
+ return result;
+
+ static Int32 getTypePrecedence(VariantTypeModel type)
+ {
+ var result = type switch
+ {
+ { Kind: VariantTypeKind.Unmanaged } => 3,
+ { Kind: VariantTypeKind.Value } => 2,
+ { Kind: VariantTypeKind.Reference or VariantTypeKind.Unknown, IsNullable: true } => 1,
+ _ => 0
+ };
+
+ return result;
+ }
+ });
+ var variants = ctx.CollectionFactory.CreateList();
+ foreach (var variant in variantsList)
+ {
+ ctx.ThrowIfCancellationRequested();
+
+ variants.Add(variant);
+ }
+
+ return variants;
+ }
+
+ private static void AppendContainingType(
+ INamedTypeSymbol type,
+ EquatableList containingTypes,
+ in ModelCreationContext ctx)
+ {
+ ctx.ThrowIfCancellationRequested();
+
+ if (type.ContainingType is { } t)
+ {
+ AppendContainingType(t, containingTypes, in ctx);
+ }
+
+ var typeParameters = ctx.CollectionFactory.CreateList();
+
+ for (var i = 0; i < type.TypeParameters.Length; i++)
+ {
+ ctx.ThrowIfCancellationRequested();
+
+ typeParameters.Add(type.TypeParameters[i].Name);
+ }
+
+ var containingType = new ContainingTypeModel(
+ $"{(type.IsRecord ? "record " : String.Empty)}{(type.TypeKind is Microsoft.CodeAnalysis.TypeKind.Struct ? "struct" : "class")}",
+ type.Name,
+ typeParameters);
+
+ containingTypes.Add(containingType);
+ }
+
+ private static void ParseTargetTypeParametersAttributes(
+ INamedTypeSymbol target,
+ List variants,
+ List variantGroups,
+ EquatableList typeParameters,
+ CancellationToken ct)
+ {
+ foreach (var typeParameter in target.TypeParameters)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ typeParameters.Add(typeParameter.Name);
+
+ var attributes = typeParameter.GetAttributes();
+ foreach (var attributeData in attributes)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (!attributeData.TryGetUnionTypeAttributeModel(
+ new UnionTypeAttribute.Model.TypeParameterState(typeParameter),
+ out var variant,
+ cancellationToken: ct))
+ {
+ continue;
+ }
+
+ variants.Add(variant);
+
+ AddDistinctVariantGroups(variant, variantGroups, ct);
+ }
+ }
+ }
+
+ private static void ParseTargetAttributes(
+ INamedTypeSymbol target,
+ List variants,
+ List variantGroups,
+ out UnionTypeSettingsAttribute.Model settings,
+ CancellationToken ct)
+ {
+ settings = UnionTypeSettingsAttribute.Model.Default;
+
+ foreach (var attributeData in target.GetAttributes())
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (attributeData.TryGetUnionTypeSettingsAttributeModel(out var s, cancellationToken: ct))
+ {
+ settings = s;
+ continue;
+ }
+
+ if (attributeData.AttributeClass?.TypeArguments is not [_, ..] typeArguments)
+ {
+ continue;
+ }
+
+ foreach (var typeArgument in typeArguments)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (!attributeData.TryGetUnionTypeAttributeModel(
+ new UnionTypeAttribute.Model.TypeArgumentState(typeArgument),
+ out var variant,
+ cancellationToken: ct))
+ {
+ continue;
+ }
+
+ variants.Add(variant);
+
+ AddDistinctVariantGroups(variant, variantGroups, ct);
+ }
+ }
+ }
+
+ private static void AddDistinctVariantGroups(
+ UnionTypeAttribute.Model variant, List variantGroups,
+ CancellationToken ct)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ foreach (var group in variant.Groups)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (!variantGroups.Contains(group))
+ {
+ variantGroups.Add(group);
+ }
+ }
+ }
+}
diff --git a/Janus.Analyzers/Models/UnionModelValidation.cs b/Janus.Analyzers/Models/UnionModelValidation.cs
new file mode 100644
index 0000000..cb04f0a
--- /dev/null
+++ b/Janus.Analyzers/Models/UnionModelValidation.cs
@@ -0,0 +1,215 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using Microsoft.CodeAnalysis;
+
+internal readonly struct UnionModelValidation(UnionModel model, CancellationToken ct)
+{
+ public Boolean HasUnsupportedSettingsUnawareStatePostCondition(INamedTypeSymbol target)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (HasUnsupportedSettingsUnawareState)
+ {
+ return true;
+ }
+
+ var unionName = target.ToDisplayString(UnionTypeAttribute.Model.TypeDisplayFormat);
+ foreach (var variant in model.Variants)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (variant.Type.Name == unionName)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public bool HasUnsupportedSettingsUnawareState
+ {
+ get
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (ExceedsVariantGroupsLimit)
+ {
+ return true;
+ }
+
+ if (ContainsDuplicateVariantNames)
+ {
+ return true;
+ }
+
+ if (ContainsObjectVariant)
+ {
+ return true;
+ }
+
+ if (ContainsValueTypeVariantAsStructUnion)
+ {
+ return true;
+ }
+
+ if (ContainsDuplicateNullableValueTypeVariants)
+ {
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ public Boolean ContainsDuplicateNullableValueTypeVariants
+ {
+ get
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var handledTypes = new HashSet();
+
+ foreach (var variant in model.Variants)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (!handledTypes.Add(variant.Type.Name))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ public Boolean ContainsValueTypeVariantAsStructUnion
+ {
+ get
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (model.TypeKind is UnionTypeKind.Class)
+ {
+ return false;
+ }
+
+ foreach (var variant in model.Variants)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (variant.Type.Name is "global::System.ValueType")
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ public Boolean ContainsObjectVariant
+ {
+ get
+ {
+ ct.ThrowIfCancellationRequested();
+
+ foreach (var variant in model.Variants)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (variant.Type.Name is "object")
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ public bool HasUnsupportedSettingsAwareState
+ {
+ get
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (IsGenericJsonSerializable)
+ {
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ public bool ExceedsVariantGroupsLimit
+ {
+ get
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (model.VariantGroups.Count > 31)
+ {
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ private bool IsGenericJsonSerializable
+ {
+ get
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (model.Settings.JsonConverterSetting is JsonConverterSetting.OmitJsonConverter)
+ {
+ return false;
+ }
+
+ var isExplicitlyGeneric = model.TypeParameters is not [];
+ if (isExplicitlyGeneric)
+ {
+ return true;
+ }
+
+ foreach (var containingType in model.ContainingTypes)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (containingType.TypeParameters is not [])
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ public bool ContainsDuplicateVariantNames
+ {
+ get
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var variantNames = new HashSet();
+ foreach (var variant in model.Variants)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ if (!variantNames.Add(variant.Name))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/Janus.Analyzers/Models/UnionTypeAttribute.Model.cs b/Janus.Analyzers/Models/UnionTypeAttribute.Model.cs
new file mode 100644
index 0000000..46e03a6
--- /dev/null
+++ b/Janus.Analyzers/Models/UnionTypeAttribute.Model.cs
@@ -0,0 +1,149 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis;
+
+using System.Runtime.InteropServices;
+using Janus;
+using Microsoft.CodeAnalysis;
+
+partial class UnionTypeAttribute
+{
+ [StructLayout(LayoutKind.Auto)]
+ partial record struct Model
+ {
+ internal static readonly SymbolDisplayFormat TypeDisplayFormat = new SymbolDisplayFormat(
+ globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.Included,
+ typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
+ genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
+ miscellaneousOptions:
+ SymbolDisplayMiscellaneousOptions.ExpandNullable |
+ SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers |
+ SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
+
+ [InitializationMethod(StateTypeName = "TypeParameterState")]
+ private void Initialize(ITypeParameterSymbol target, CancellationToken ct)
+ {
+ Name ??= target.Name;
+ var name = target.Name;
+ var docsId = target.GetDocumentationCommentId() ?? String.Empty;
+ Type = target switch
+ {
+ { HasUnmanagedTypeConstraint: true } =>
+ new(VariantTypeKind.Unmanaged,
+ IsNullable: false,
+ IsInterface: false,
+ Name: name,
+ DocsId: docsId),
+ { HasValueTypeConstraint: true } =>
+ new(VariantTypeKind.Value,
+ IsNullable: false,
+ IsInterface: false,
+ Name: name,
+ DocsId: docsId),
+ { HasReferenceTypeConstraint: true, HasNotNullConstraint: true } =>
+ new(VariantTypeKind.Reference,
+ IsNullable: false,
+ IsInterface: false,
+ Name: name,
+ DocsId: docsId),
+ { HasReferenceTypeConstraint: true, HasNotNullConstraint: false } =>
+ new(VariantTypeKind.Reference,
+ IsNullable: true,
+ IsInterface: false,
+ Name: name,
+ DocsId: docsId),
+ _ =>
+ new(VariantTypeKind.Unknown,
+ IsNullable: false,
+ IsInterface: false,
+ Name: name,
+ DocsId: docsId),
+ };
+ }
+
+ [InitializationMethod(StateTypeName = "TypeArgumentState")]
+ private void Initialize(ITypeSymbol variant, CancellationToken ct)
+ {
+ var isInterface = variant.TypeKind is TypeKind.Interface;
+ var docsId = variant.GetDocumentationCommentId() ?? String.Empty;
+
+ if (variant is INamedTypeSymbol
+ {
+ OriginalDefinition:
+ {
+ SpecialType: SpecialType.System_Nullable_T
+ },
+ TypeArguments: [{ } actualVariant]
+ })
+ {
+ Name ??= actualVariant.Name;
+ var name = actualVariant.ToDisplayString(TypeDisplayFormat);
+ Type = new(
+ actualVariant.IsUnmanagedType ? VariantTypeKind.Unmanaged : VariantTypeKind.Value,
+ IsNullable: true,
+ IsInterface: isInterface,
+ Name: name,
+ DocsId: docsId);
+ }
+ else
+ {
+ Name ??= variant.Name;
+ var name = variant.ToDisplayString(TypeDisplayFormat);
+ Type = variant switch
+ {
+ { IsUnmanagedType: true } =>
+ new(VariantTypeKind.Unmanaged,
+ IsNullable: false,
+ IsInterface: isInterface,
+ Name: name,
+ DocsId: docsId),
+ { IsValueType: true } =>
+ new(VariantTypeKind.Value,
+ IsNullable: false,
+ IsInterface: isInterface,
+ Name: name,
+ DocsId: docsId),
+ { IsReferenceType: true } =>
+ new(VariantTypeKind.Reference,
+ IsNullable: IsNullable,
+ IsInterface: isInterface,
+ Name: name,
+ DocsId: docsId),
+ _ =>
+ new(VariantTypeKind.Unknown,
+ IsNullable: false,
+ IsInterface: false,
+ Name: name,
+ DocsId: docsId),
+ };
+ }
+ }
+
+ public VariantTypeModel Type { get; private set; }
+ }
+
+ [DefaultValue((String[])[])]
+ public override String[] Groups
+ {
+ get => base.Groups;
+ set => base.Groups = value;
+ }
+
+ public override String? Name
+ {
+ get => base.Name;
+ set => base.Name = value;
+ }
+
+ public override String? Description
+ {
+ get => base.Description;
+ set => base.Description = value;
+ }
+
+ public override Boolean IsNullable
+ {
+ get => base.IsNullable;
+ set => base.IsNullable = value;
+ }
+}
diff --git a/Janus.Analyzers/Models/UnionTypeKind.cs b/Janus.Analyzers/Models/UnionTypeKind.cs
new file mode 100644
index 0000000..2b3767c
--- /dev/null
+++ b/Janus.Analyzers/Models/UnionTypeKind.cs
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+internal enum UnionTypeKind : byte
+{
+ Class,
+ Struct
+}
diff --git a/Janus.Analyzers/Models/UnionTypeSettingsAttribute.Model.cs b/Janus.Analyzers/Models/UnionTypeSettingsAttribute.Model.cs
new file mode 100644
index 0000000..4c80586
--- /dev/null
+++ b/Janus.Analyzers/Models/UnionTypeSettingsAttribute.Model.cs
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis;
+
+partial class UnionTypeSettingsAttribute
+{
+ partial record struct Model
+ {
+ public static Model InheritanceRoot { get; } = new()
+ {
+ EqualityOperatorsSetting = EqualityOperatorsSetting.EmitOperatorsIfValueType,
+ ToStringSetting = ToStringSetting.Detailed,
+ JsonConverterSetting = JsonConverterSetting.OmitJsonConverter
+ };
+
+ public void Inherit(Model source)
+ {
+ if (EqualityOperatorsSetting is EqualityOperatorsSetting.Inherit)
+ {
+ EqualityOperatorsSetting = source.EqualityOperatorsSetting;
+ }
+
+ if (ToStringSetting is ToStringSetting.Inherit)
+ {
+ ToStringSetting = source.ToStringSetting;
+ }
+
+ if (JsonConverterSetting is JsonConverterSetting.Inherit)
+ {
+ JsonConverterSetting = source.JsonConverterSetting;
+ }
+ }
+ }
+}
diff --git a/Janus.Analyzers/Models/VariantTypeKind.cs b/Janus.Analyzers/Models/VariantTypeKind.cs
new file mode 100644
index 0000000..08a68af
--- /dev/null
+++ b/Janus.Analyzers/Models/VariantTypeKind.cs
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+internal enum VariantTypeKind : byte
+{
+ Unmanaged,
+ Value,
+ Reference,
+ Unknown
+}
diff --git a/Janus.Analyzers/Models/VariantTypeModel.cs b/Janus.Analyzers/Models/VariantTypeModel.cs
new file mode 100644
index 0000000..11af356
--- /dev/null
+++ b/Janus.Analyzers/Models/VariantTypeModel.cs
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+internal readonly record struct VariantTypeModel(
+ VariantTypeKind Kind,
+ Boolean IsNullable,
+ Boolean IsInterface,
+ String Name,
+ String DocsId)
+{
+ public String NullableName { get; } = IsNullable || Kind is VariantTypeKind.Unknown ? Name + "?" : Name;
+
+ public String NullableValueTypeName =>
+ Kind is VariantTypeKind.Reference or VariantTypeKind.Unknown ? Name : NullableName;
+
+ public Boolean Equals(VariantTypeModel other) =>
+ other.Kind == Kind
+ && other.IsNullable == IsNullable
+ && other.Name == Name;
+
+ public override Int32 GetHashCode() =>
+ HashCode.Combine(Kind, IsNullable, Name);
+
+ public override String ToString() => throw new NotSupportedException();
+}
diff --git a/Janus.Analyzers/Properties/launchSettings.json b/Janus.Analyzers/Properties/launchSettings.json
new file mode 100644
index 0000000..fe65332
--- /dev/null
+++ b/Janus.Analyzers/Properties/launchSettings.json
@@ -0,0 +1,8 @@
+{
+ "profiles": {
+ "EndToEnd.Tests": {
+ "commandName": "DebugRoslynComponent",
+ "targetProject": "../Janus.EndToEnd.Tests/Janus.EndToEnd.Tests.csproj"
+ }
+ }
+}
diff --git a/Janus.Analyzers/UnionTypeAttribute.cs b/Janus.Analyzers/UnionTypeAttribute.cs
new file mode 100644
index 0000000..a4dff0c
--- /dev/null
+++ b/Janus.Analyzers/UnionTypeAttribute.cs
@@ -0,0 +1,146 @@
+//
+// SPDX-License-Identifier: MPL-2.0
+//
+#nullable enable
+#pragma warning disable
+
+namespace RhoMicro.CodeAnalysis
+{
+ ///
+ /// Marks the target type as a union type.
+ ///
+ abstract class UnionTypeBaseAttribute : global::System.Attribute
+ {
+ ///
+ /// Gets or sets the groups that the variant is to be a part of.
+ /// Variants that share a group may be checked for using e.g.:
+ /// myUnion.Variant.Groups.ContainsMyGroup where MyGroup
+ /// is the name of the group that the variant is a part of.
+ ///
+ public virtual string[] Groups { get; set; } = global::System.Array.Empty();
+ }
+
+ ///
+ /// Marks the target type as a union type.
+ ///
+ abstract class NamedUnionTypeBaseAttribute : UnionTypeBaseAttribute
+ {
+ ///
+ /// Gets or sets the name to use for members representing the variant of the union.
+ /// For example, the variant would be represented using names like
+ /// List_of_T. Setting this property to MyName will instruct the generator to use
+ /// member names like MyName instead of List_of_T. Use this property to avoid
+ /// name collisions in generated code. Since the name will be used for member names, it is
+ /// required to also be a valid C# identifier.
+ ///
+ public virtual string? Name { get; set; }
+
+ ///
+ /// Gets or sets the description of the variant.
+ /// This value will be used in documentation comments, so escaping special characters might be necessary.
+ ///
+ public virtual string? Description { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether to generate the reference type variant as nullable.
+ /// This setting is only relevant for reference types or generic type parameters not constrained
+ /// to . In order to use nullable value types, use a
+ /// variant type.
+ ///
+ public virtual bool IsNullable { get; set; }
+ }
+
+ ///
+ /// Marks the target type as a union type with the variant .
+ ///
+ [global::System.AttributeUsage(global::System.AttributeTargets.Struct | global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ sealed partial class UnionTypeAttribute : NamedUnionTypeBaseAttribute
+ { }
+ ///
+ /// Marks the target type as a union type with the variants
+ ///
+ /// and .
+ ///
+ [global::System.AttributeUsage(global::System.AttributeTargets.Struct | global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ sealed partial class UnionTypeAttribute : UnionTypeBaseAttribute
+ { }
+ ///
+ /// Marks the target type as a union type with the variants
+ /// ,
+ ///
+ /// and .
+ ///
+ [global::System.AttributeUsage(global::System.AttributeTargets.Struct | global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ sealed partial class UnionTypeAttribute : UnionTypeBaseAttribute
+ { }
+ ///
+ /// Marks the target type as a union type with the variants
+ /// ,
+ /// ,
+ ///
+ /// and .
+ ///
+ [global::System.AttributeUsage(global::System.AttributeTargets.Struct | global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ sealed partial class UnionTypeAttribute : UnionTypeBaseAttribute
+ { }
+ ///
+ /// Marks the target type as a union type with the variants
+ /// ,
+ /// ,
+ /// ,
+ ///
+ /// and .
+ ///
+ [global::System.AttributeUsage(global::System.AttributeTargets.Struct | global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ sealed partial class UnionTypeAttribute : UnionTypeBaseAttribute
+ { }
+ ///
+ /// Marks the target type as a union type with the variants
+ /// ,
+ /// ,
+ /// ,
+ /// ,
+ ///
+ /// and .
+ ///
+ [global::System.AttributeUsage(global::System.AttributeTargets.Struct | global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ sealed partial class UnionTypeAttribute : UnionTypeBaseAttribute
+ { }
+ ///
+ /// Marks the target type as a union type with the variants
+ /// ,
+ /// ,
+ /// ,
+ /// ,
+ /// ,
+ ///
+ /// and .
+ ///
+ [global::System.AttributeUsage(global::System.AttributeTargets.Struct | global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ sealed partial class UnionTypeAttribute : UnionTypeBaseAttribute
+ { }
+ ///
+ /// Marks the target type as a union type with the variants
+ /// ,
+ /// ,
+ /// ,
+ /// ,
+ /// ,
+ /// ,
+ ///
+ /// and .
+ ///
+ [global::System.AttributeUsage(global::System.AttributeTargets.Struct | global::System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+ sealed partial class UnionTypeAttribute : UnionTypeBaseAttribute
+ { }
+ ///
+ /// Marks the target type as a union type with one of the variants being the annotated type parameter.
+ ///
+ [global::System.AttributeUsage(global::System.AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
+#if RHOMICRO_CODEANALYSIS_JANUS_ANALYZERS
+ [global::RhoMicro.CodeAnalysis.IncludeFile]
+ [RhoMicro.CodeAnalysis.GenerateFactory(GenerateModelTypeAsStruct = true)]
+#endif
+ sealed partial class UnionTypeAttribute : NamedUnionTypeBaseAttribute
+ { }
+}
diff --git a/Janus.Analyzers/UnionTypeSettingsAttribute.cs b/Janus.Analyzers/UnionTypeSettingsAttribute.cs
new file mode 100644
index 0000000..ae3019e
--- /dev/null
+++ b/Janus.Analyzers/UnionTypeSettingsAttribute.cs
@@ -0,0 +1,126 @@
+//
+// SPDX-License-Identifier: MPL-2.0
+//
+#nullable enable
+#pragma warning disable
+
+namespace RhoMicro.CodeAnalysis
+{
+ ///
+ /// Defines settings for generating an implementation of .
+ ///
+ enum ToStringSetting
+ {
+ ///
+ /// Inherits the setting. This is the default value.
+ ///
+ /// - If the target is a type, it will inherit the setting from its containing assembly.
+ /// - If the target is an assembly, the setting will be used.
+ ///
+ ///
+ Inherit,
+ ///
+ /// The generator will emit an implementation that returns detailed information, including:
+ ///
+ /// - the name of the union type
+ /// - the set of variants
+ /// - an indication of which variant is being represented by the instance
+ /// - the value currently being represented by the instance
+ ///
+ ///
+ Detailed,
+ ///
+ /// The generator will not generate an implementation of .
+ ///
+ None,
+ ///
+ /// The generator will generate an implementation that returns the result of calling on the currently represented value.
+ ///
+ Simple
+ }
+
+ ///
+ /// Defines settings pertaining to equality operator implementations.
+ ///
+ enum EqualityOperatorsSetting
+ {
+ ///
+ /// Inherits the setting. This is the default value.
+ ///
+ /// - If the target is a type, it will inherit the setting from its containing assembly.
+ /// - If the target is an assembly, the setting will be used.
+ ///
+ ///
+ Inherit,
+ ///
+ /// Equality operators will be emitted only if the target union type is a value type.
+ ///
+ EmitOperatorsIfValueType,
+ ///
+ /// Equality operators will be emitted.
+ ///
+ EmitOperators,
+ ///
+ /// Equality operators will be omitted.
+ ///
+ OmitOperators
+ }
+
+ ///
+ /// Defiles settings pertaining to JSON converter implementations.
+ ///
+ enum JsonConverterSetting
+ {
+ ///
+ /// Inherits the setting. This is the default value.
+ ///
+ /// - If the target is a type, it will inherit the setting from its containing assembly.
+ /// - If the target is an assembly, the setting will be used.
+ ///
+ ///
+ Inherit,
+ ///
+ /// No JSON converter implementation is emitted.
+ ///
+ OmitJsonConverter,
+ ///
+ /// A JSON converter implementation is emitted.
+ ///
+ EmitJsonConverter
+ }
+
+ ///
+ /// Supplies the generator with additional settings on how to generate a targeted union type.
+ /// If the target member is an assembly, the attribute supplies default values for any union
+ /// type setting not defined.
+ ///
+ [global::System.AttributeUsage(global::System.AttributeTargets.Struct | global::System.AttributeTargets.Class | global::System.AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)]
+#if RHOMICRO_CODEANALYSIS_JANUS_ANALYZERS
+ [global::RhoMicro.CodeAnalysis.IncludeFile]
+ [RhoMicro.CodeAnalysis.GenerateFactory(GenerateModelTypeAsStruct = true)]
+#endif
+ sealed partial class UnionTypeSettingsAttribute : global::System.Attribute
+ {
+ ///
+ /// Defines how to generate an implementation for .
+ ///
+ public ToStringSetting ToStringSetting { get; set; }
+ ///
+ /// Indicates how to generate equality operators.
+ /// By default, equality operators will only be emitted for value types, to preserve
+ /// reference equality for comparing reference union types via == or !=.
+ ///
+ public EqualityOperatorsSetting EqualityOperatorsSetting { get; set; }
+
+
+ ///
+ /// Gets or sets a value indicating whether to make the union type JSON serializable.
+ ///
+ ///
+ /// The generated JSON serialization support is not AOT-compatible.
+ /// If you require this as a feature, please open an issue with the maintainer.
+ ///
+ public JsonConverterSetting JsonConverterSetting { get; set; }
+
+ }
+}
diff --git a/Janus.CodeFixes/Janus.CodeFixes.csproj b/Janus.CodeFixes/Janus.CodeFixes.csproj
new file mode 100644
index 0000000..0fe721c
--- /dev/null
+++ b/Janus.CodeFixes/Janus.CodeFixes.csproj
@@ -0,0 +1,19 @@
+
+
+
+ netstandard2.0
+ preview
+ enable
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Janus.CodeFixes/JanusCodeFixProvider.cs b/Janus.CodeFixes/JanusCodeFixProvider.cs
new file mode 100644
index 0000000..80162d3
--- /dev/null
+++ b/Janus.CodeFixes/JanusCodeFixProvider.cs
@@ -0,0 +1,124 @@
+using System;
+
+namespace RhoMicro.CodeAnalysis.Janus;
+
+using System.Collections.Immutable;
+using System.Composition;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+///
+/// Provides code fixes for the Janus analyzer.
+///
+[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(JanusCodeFixProvider)), Shared]
+public class JanusCodeFixProvider : CodeFixProvider
+{
+ ///
+ public sealed override ImmutableArray FixableDiagnosticIds { get; } =
+ [
+ DiagnosticIds.ClassUnionsShouldBeSealed
+ ];
+
+ ///
+ public override FixAllProvider GetFixAllProvider()
+ => FixAllProvider.Create(async (context, document, diagnostics) =>
+ {
+ var ct = context.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ if (diagnostics.IsEmpty)
+ {
+ return null;
+ }
+
+ return await GetTransformedDocumentAsync(
+ document,
+ diagnostics,
+ context.CancellationToken)
+ .ConfigureAwait(false);
+ });
+
+ ///
+ public override Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var ct = context.CancellationToken;
+
+ ct.ThrowIfCancellationRequested();
+
+ foreach (var diagnostic in context.Diagnostics)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ context.RegisterCodeFix(
+ CodeAction.Create(
+ "Seal union type",
+ cancellationToken => GetTransformedDocumentAsync(context.Document, [diagnostic], cancellationToken),
+ equivalenceKey: nameof(JanusCodeFixProvider)),
+ diagnostic);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private static async Task GetTransformedDocumentAsync(
+ Document document,
+ ImmutableArray diagnosticsToFix,
+ CancellationToken ct)
+ {
+ ct.ThrowIfCancellationRequested();
+
+ var syntaxRoot = await document.GetSyntaxRootAsync(ct);
+
+ if (syntaxRoot is null)
+ {
+ return document;
+ }
+
+ var nodesToReplace = diagnosticsToFix
+ .Select(d =>
+ {
+ var result = syntaxRoot.FindNode(d.Location.SourceSpan) is TypeDeclarationSyntax s
+ ? s
+ : null;
+
+ return result;
+ })
+ .OfType();
+
+ var transformedSyntaxRoot = syntaxRoot.ReplaceNodes(
+ nodesToReplace,
+ (_, t) =>
+ {
+ if (t.Modifiers.Any(SyntaxKind.SealedKeyword))
+ {
+ return t;
+ }
+
+ SyntaxTokenList modifiedModifiers;
+ var sealedToken = SyntaxFactory.Token(SyntaxKind.SealedKeyword);
+ if (t.Modifiers.FirstOrDefault(st => st.IsKind(SyntaxKind.PartialKeyword)) is
+ {
+ RawKind: (Int32)SyntaxKind.PartialKeyword
+ } partialToken)
+ {
+ modifiedModifiers = t.Modifiers.ReplaceRange(partialToken, [sealedToken, partialToken]);
+ }
+ else
+ {
+ modifiedModifiers = t.Modifiers.Add(sealedToken);
+ }
+
+ var modifiedNode = t.WithModifiers(modifiedModifiers);
+
+ return modifiedNode;
+ });
+
+ var transformedDocument = document.WithSyntaxRoot(transformedSyntaxRoot);
+
+ return transformedDocument;
+ }
+}
diff --git a/Janus.Library/IUnion.cs b/Janus.Library/IUnion.cs
new file mode 100644
index 0000000..ffc4ffb
--- /dev/null
+++ b/Janus.Library/IUnion.cs
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis;
+
+using System.Diagnostics.CodeAnalysis;
+
+///
+/// Represents a union type.
+///
+public interface IUnion
+{
+ ///
+ /// Maps the variant value represented by this instance to a union of type .
+ ///
+ ///
+ /// The factory to use when creating an instance of .
+ ///
+ ///
+ /// The type of union to map to.
+ ///
+ ///
+ /// The type of factory to use when creating an instance of .
+ ///
+ ///
+ /// A new instance of representing the value represented by this instance.
+ ///
+ TUnion MapTo(TFactory factory)
+ where TFactory : IUnionFactory;
+
+
+ ///
+ /// Attempts to map the variant value represented by this instance to a union of type .
+ ///
+ ///
+ /// The factory to use when attempting to create an instance of .
+ ///
+ ///
+ /// A new instance of representing the value represented by this instance,
+ /// if the factory indicates success; otherwise, .
+ ///
+ ///
+ /// The type of union to attempt to map to.
+ ///
+ ///
+ /// The type of factory to use when attempting to create an instance of .
+ ///
+ ///
+ /// if the factory indicates success; otherwise, .
+ ///
+ bool TryMapTo(TFactory factory, [NotNullWhen(true)] out TUnion? union)
+ where TFactory : IUnionFactory;
+}
diff --git a/Janus.Library/IUnionFactory.cs b/Janus.Library/IUnionFactory.cs
new file mode 100644
index 0000000..f1fa0d3
--- /dev/null
+++ b/Janus.Library/IUnionFactory.cs
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis;
+
+using System.Diagnostics.CodeAnalysis;
+
+///
+/// Creates instances of .
+///
+///
+/// The type of union to create.
+///
+public interface IUnionFactory
+{
+ ///
+ /// Creates a new instance of from a variant value.
+ ///
+ ///
+ /// The value to create an instance of from.
+ ///
+ ///
+ /// The type of variant to create an instance of from.
+ ///
+ ///
+ /// A new instance of , containing .
+ ///
+ TUnion Create(TVariant value);
+
+ ///
+ /// Attempts to create a new instance of from a variant value.
+ ///
+ ///
+ /// The value to attempt to create an instance of from.
+ ///
+ ///
+ /// The created instance of if
+ /// can represent ; otherwise, .
+ ///
+ ///
+ /// The type of the variant to create an instance of from.
+ ///
+ ///
+ /// if an instance of could be created;
+ /// otherwise, .
+ ///
+ bool TryCreate(TVariant value, [NotNullWhen(true)] out TUnion? union);
+}
diff --git a/Janus.Library/Janus.Library.csproj b/Janus.Library/Janus.Library.csproj
new file mode 100644
index 0000000..83a6cd8
--- /dev/null
+++ b/Janus.Library/Janus.Library.csproj
@@ -0,0 +1,16 @@
+
+
+
+ netstandard2.0
+ preview
+ enable
+
+
+
+
+
+
+
+
+
+
diff --git a/Janus.Library/NotNullWhenAttribute.cs b/Janus.Library/NotNullWhenAttribute.cs
new file mode 100644
index 0000000..b46b7f9
--- /dev/null
+++ b/Janus.Library/NotNullWhenAttribute.cs
@@ -0,0 +1,28 @@
+#pragma warning disable
+#nullable enable annotations
+
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Diagnostics.CodeAnalysis
+{
+ ///
+ /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it.
+ ///
+ [global::System.AttributeUsage(global::System.AttributeTargets.Parameter, Inherited = false)]
+ [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ internal sealed class NotNullWhenAttribute : global::System.Attribute
+ {
+ ///
+ /// Initializes the attribute with the specified return value condition.
+ ///
+ /// The return value condition. If the method returns this value, the associated parameter will not be null.
+ public NotNullWhenAttribute(bool returnValue)
+ {
+ ReturnValue = returnValue;
+ }
+
+ /// Gets the return value condition.
+ public bool ReturnValue { get; }
+ }
+}
diff --git a/Janus.Library/README.md b/Janus.Library/README.md
new file mode 100644
index 0000000..199a7e8
--- /dev/null
+++ b/Janus.Library/README.md
@@ -0,0 +1 @@
+# RhoMicro.CodeAnalysis.Janus.Library
diff --git a/Janus.TestApplication/Janus.TestApplication.csproj b/Janus.TestApplication/Janus.TestApplication.csproj
new file mode 100644
index 0000000..2e80e4d
--- /dev/null
+++ b/Janus.TestApplication/Janus.TestApplication.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net10.0
+ preview
+ enable
+ enable
+ false
+ true
+
+
+
+
+
+
+
diff --git a/Janus.TestApplication/Program.cs b/Janus.TestApplication/Program.cs
new file mode 100644
index 0000000..7bfd698
--- /dev/null
+++ b/Janus.TestApplication/Program.cs
@@ -0,0 +1,58 @@
+// See https://aka.ms/new-console-template for more information
+
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Text.Json;
+using RhoMicro.CodeAnalysis;
+using TestApplication;
+
+var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
+{
+ PropertyNamingPolicy = new CustomNamingPolicy(), PropertyNameCaseInsensitive = true,
+};
+
+var union = IntDoubleString.Create(42d);
+union = "foo";
+union = 47;
+
+var typeName = union.Switch(
+ onDouble: _ => "double",
+ onInt32: _ => "int",
+ onImmutableArray: _ => "array",
+ onString: _ => "string",
+ onFile: _ => "file");
+
+if (union.IsDouble)
+{
+ var d = union.AsDouble;
+}
+
+Console.WriteLine(union);
+var serialized = JsonSerializer.Serialize(union, options);
+Console.WriteLine(serialized);
+serialized = serialized.ToLower().Replace("47", "[9,8,7]").Replace("2", "3");
+Console.WriteLine(serialized);
+var deserialized =
+ JsonSerializer.Deserialize(
+ serialized, options);
+Console.WriteLine(deserialized);
+
+internal sealed class CustomNamingPolicy : JsonNamingPolicy
+{
+ public override String ConvertName(String name)
+ {
+ var sb = new StringBuilder();
+
+ for (var i = 0; i < name.Length; i++)
+ {
+ if (i is not 0)
+ {
+ sb.Append('-');
+ }
+
+ sb.Append(name[i]);
+ }
+
+ return sb.ToString();
+ }
+}
diff --git a/Janus.TestLibrary/IntDoubleString.cs b/Janus.TestLibrary/IntDoubleString.cs
new file mode 100644
index 0000000..2e0a6e5
--- /dev/null
+++ b/Janus.TestLibrary/IntDoubleString.cs
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace TestApplication;
+
+using System.Collections.Immutable;
+using RhoMicro.CodeAnalysis;
+
+[UnionType, string>(Groups = ["Foo"])]
+[UnionType(IsNullable = true, Name = "File", Description = "a file stream")]
+[UnionTypeSettings(JsonConverterSetting = JsonConverterSetting.EmitJsonConverter)]
+public sealed partial class IntDoubleString;
diff --git a/Janus.TestLibrary/Janus.TestLibrary.csproj b/Janus.TestLibrary/Janus.TestLibrary.csproj
new file mode 100644
index 0000000..8d7cc78
--- /dev/null
+++ b/Janus.TestLibrary/Janus.TestLibrary.csproj
@@ -0,0 +1,23 @@
+
+
+
+ netstandard2.0
+ preview
+ enable
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
diff --git a/UnionsGenerator.EndToEnd.Tests/AsPropertyTests.cs b/Janus.Tests.EndToEnd/AsPropertyTests.cs
similarity index 83%
rename from UnionsGenerator.EndToEnd.Tests/AsPropertyTests.cs
rename to Janus.Tests.EndToEnd/AsPropertyTests.cs
index c3eebc8..72c20e7 100644
--- a/UnionsGenerator.EndToEnd.Tests/AsPropertyTests.cs
+++ b/Janus.Tests.EndToEnd/AsPropertyTests.cs
@@ -2,15 +2,15 @@
#pragma warning disable IDE0059 // Unnecessary assignment of a value
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
-namespace RhoMicro.CodeAnalysis.UnionsGenerator.EndToEnd.Tests;
+namespace RhoMicro.CodeAnalysis.Janus.EndToEnd.Tests;
using System;
public partial class AsPropertyTests
{
- [UnionType(Alias = "Int")]
+ [UnionType(Name = "Int")]
[UnionType>]
- private partial class Union<[UnionType(Alias = "ValueT")] T>
+ private sealed partial class Union<[UnionType(Name = "ValueT")] T>
where T : struct;
[Fact]
@@ -37,20 +37,20 @@ public void NotAsIntWhenRepresentingByte()
public void NotAsListWhenRepresentingInt32()
{
Union u = 32;
- Assert.Equal(default, u.AsList_of_String);
+ Assert.Equal(default, u.AsList);
}
[Fact]
public void AsListWhenRepresentingList()
{
var expected = new List();
Union u = expected;
- Assert.Equal(expected, u.AsList_of_String);
+ Assert.Equal(expected, u.AsList);
}
[Fact]
public void NotAsListWhenRepresentingByte()
{
Union u = (Byte)32;
- Assert.Equal(default, u.AsList_of_String);
+ Assert.Equal(default, u.AsList);
}
[Fact]
diff --git a/Janus.Tests.EndToEnd/EqualityTests.cs b/Janus.Tests.EndToEnd/EqualityTests.cs
new file mode 100644
index 0000000..19a6763
--- /dev/null
+++ b/Janus.Tests.EndToEnd/EqualityTests.cs
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus.EndToEnd.Tests;
+
+using RhoMicro.CodeAnalysis;
+using System;
+
+public partial class EqualityTests
+{
+ [UnionType]
+ private sealed partial class Foo;
+
+ [UnionType]
+ private readonly partial struct StructUnion;
+
+ [Fact]
+ public void EqualsIsValueEquality()
+ {
+ Foo expected = 32;
+ Foo actual = 32;
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public void StructUnionHasEqualityOperator()
+ {
+ StructUnion expected = 32;
+ StructUnion actual = 32;
+ Assert.True(expected == actual);
+ }
+}
diff --git a/Janus.Tests.EndToEnd/FactoryTests.cs b/Janus.Tests.EndToEnd/FactoryTests.cs
new file mode 100644
index 0000000..efd9044
--- /dev/null
+++ b/Janus.Tests.EndToEnd/FactoryTests.cs
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus.EndToEnd.Tests;
+using System;
+
+public partial class FactoryTests
+{
+ [UnionType]
+ private sealed partial class UnnamedFactoryUnion;
+
+ [Fact]
+ public void UsesDefaultFactoryName()
+ {
+ _ = UnnamedFactoryUnion.Create(0);
+ }
+}
diff --git a/UnionsGenerator.Tests/GlobalUsings.cs b/Janus.Tests.EndToEnd/GlobalUsings.cs
similarity index 56%
rename from UnionsGenerator.Tests/GlobalUsings.cs
rename to Janus.Tests.EndToEnd/GlobalUsings.cs
index 0b52112..1b42ec1 100644
--- a/UnionsGenerator.Tests/GlobalUsings.cs
+++ b/Janus.Tests.EndToEnd/GlobalUsings.cs
@@ -1,4 +1,3 @@
// SPDX-License-Identifier: MPL-2.0
global using Xunit;
-global using RhoMicro.CodeAnalysis.Library;
diff --git a/UnionsGenerator.EndToEnd.Tests/IsPropertyTests.cs b/Janus.Tests.EndToEnd/IsPropertyTests.cs
similarity index 83%
rename from UnionsGenerator.EndToEnd.Tests/IsPropertyTests.cs
rename to Janus.Tests.EndToEnd/IsPropertyTests.cs
index ca4cfcb..874b8b2 100644
--- a/UnionsGenerator.EndToEnd.Tests/IsPropertyTests.cs
+++ b/Janus.Tests.EndToEnd/IsPropertyTests.cs
@@ -2,15 +2,15 @@
#pragma warning disable IDE0059 // Unnecessary assignment of a value
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
-namespace RhoMicro.CodeAnalysis.UnionsGenerator.EndToEnd.Tests;
+namespace RhoMicro.CodeAnalysis.Janus.EndToEnd.Tests;
using System;
public partial class IsPropertyTests
{
- [UnionType(Alias = "Int")]
+ [UnionType(Name = "Int")]
[UnionType>]
- private partial class Union<[UnionType] T>;
+ private sealed partial class Union<[UnionType] T>;
[Fact]
public void IsIntWhenRepresentingInt32()
@@ -35,19 +35,19 @@ public void IsNotIntWhenRepresentingByte()
public void IsNotListWhenRepresentingInt32()
{
Union u = 32;
- Assert.False(u.IsList_of_String);
+ Assert.False(u.IsList);
}
[Fact]
public void IsListWhenRepresentingList()
{
Union u = new List();
- Assert.True(u.IsList_of_String);
+ Assert.True(u.IsList);
}
[Fact]
public void IsNotListWhenRepresentingByte()
{
Union u = (Byte)32;
- Assert.False(u.IsList_of_String);
+ Assert.False(u.IsList);
}
[Fact]
diff --git a/UnionsGenerator.EndToEnd.Tests/UnionsGenerator.EndToEnd.Tests.csproj b/Janus.Tests.EndToEnd/Janus.Tests.EndToEnd.csproj
similarity index 68%
rename from UnionsGenerator.EndToEnd.Tests/UnionsGenerator.EndToEnd.Tests.csproj
rename to Janus.Tests.EndToEnd/Janus.Tests.EndToEnd.csproj
index f81b108..2001c2e 100644
--- a/UnionsGenerator.EndToEnd.Tests/UnionsGenerator.EndToEnd.Tests.csproj
+++ b/Janus.Tests.EndToEnd/Janus.Tests.EndToEnd.csproj
@@ -1,10 +1,12 @@
- net8.0
+ net10.0
enable
enable
true
+ true
+ $(NoWarn);CS1591
@@ -21,7 +23,9 @@
-
+
+
+
diff --git a/UnionsGenerator.EndToEnd.Tests/JsonConverterTests.cs b/Janus.Tests.EndToEnd/JsonConverterTests.cs
similarity index 64%
rename from UnionsGenerator.EndToEnd.Tests/JsonConverterTests.cs
rename to Janus.Tests.EndToEnd/JsonConverterTests.cs
index cbdad3b..164868e 100644
--- a/UnionsGenerator.EndToEnd.Tests/JsonConverterTests.cs
+++ b/Janus.Tests.EndToEnd/JsonConverterTests.cs
@@ -1,28 +1,30 @@
// SPDX-License-Identifier: MPL-2.0
-namespace RhoMicro.CodeAnalysis.UnionsGenerator.EndToEnd.Tests;
+namespace RhoMicro.CodeAnalysis.Janus.EndToEnd.Tests;
+
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
-using System.Text;
using System.Text.Json;
-using System.Threading.Tasks;
public partial class JsonConverterTests
{
[UnionType>]
+ [UnionTypeSettings(JsonConverterSetting = JsonConverterSetting.EmitJsonConverter)]
private readonly partial struct Union;
[Fact]
public void SerializesDateTimeUnion()
{
- var expected = DateTime.Parse("01/10/2009 7:34");
+ var expected = DateTime.Parse("01/10/2009 7:34", CultureInfo.InvariantCulture);
Union u = expected;
var serialized = JsonSerializer.Serialize(u);
var deserialized = JsonSerializer.Deserialize(serialized);
- Assert.True(deserialized.IsDateTime);
- Assert.Equal(expected, deserialized.AsDateTime);
+ Assert.True(deserialized.TryCastToDateTime(out var actual));
+ Assert.Equal(expected, actual);
}
+
[Fact]
public void SerializesDoubleUnion()
{
@@ -30,9 +32,10 @@ public void SerializesDoubleUnion()
Union u = expected;
var serialized = JsonSerializer.Serialize(u);
var deserialized = JsonSerializer.Deserialize(serialized);
- Assert.True(deserialized.IsDouble);
- Assert.Equal(expected, deserialized.AsDouble);
+ Assert.True(deserialized.TryCastToDouble(out var actual));
+ Assert.Equal(expected, actual);
}
+
[Fact]
public void SerializesStringUnion()
{
@@ -40,9 +43,10 @@ public void SerializesStringUnion()
Union u = expected;
var serialized = JsonSerializer.Serialize(u);
var deserialized = JsonSerializer.Deserialize(serialized);
- Assert.True(deserialized.IsString);
- Assert.Equal(expected, deserialized.AsString);
+ Assert.True(deserialized.TryCastToString(out var actual));
+ Assert.Equal(expected, actual);
}
+
[Fact]
public void SerializesListUnion()
{
@@ -50,7 +54,7 @@ public void SerializesListUnion()
Union u = expected;
var serialized = JsonSerializer.Serialize(u);
var deserialized = JsonSerializer.Deserialize(serialized);
- Assert.True(deserialized.IsList_of_String);
- Assert.True(expected.SequenceEqual(deserialized.AsList_of_String!));
+ Assert.True(deserialized.TryCastToList(out var actual));
+ Assert.True(expected.SequenceEqual(actual));
}
}
diff --git a/Janus.Tests.EndToEnd/NullableTests.cs b/Janus.Tests.EndToEnd/NullableTests.cs
new file mode 100644
index 0000000..25ae2e9
--- /dev/null
+++ b/Janus.Tests.EndToEnd/NullableTests.cs
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: MPL-2.0
+
+namespace RhoMicro.CodeAnalysis.Janus.EndToEnd.Tests;
+
+using System;
+
+public partial class NullableTests
+{
+ [UnionType]
+ private readonly partial struct NullableBoolUnion;
+
+ [Fact]
+ public void NullableBoolTrueFactoryCall()
+ {
+ var u = NullableBoolUnion.Create((Boolean?)true);
+ Assert.True(u.IsBoolean);
+ Assert.True(u.AsBoolean.HasValue);
+ Assert.True(u.AsBoolean.Value);
+ }
+
+ [Fact]
+ public void NullableBoolFalseFactoryCall()
+ {
+ var u = NullableBoolUnion.Create((Boolean?)false);
+ Assert.True(u.IsBoolean);
+ Assert.True(u.AsBoolean.HasValue);
+ Assert.False(u.AsBoolean.Value);
+ }
+
+ [Fact]
+ public void NullableBoolNullFactoryCall()
+ {
+ var u = NullableBoolUnion.Create((Boolean?)null);
+ Assert.True(u.IsBoolean);
+ Assert.False(u.AsBoolean.HasValue);
+ }
+}
diff --git a/UnionsGenerator.EndToEnd.Tests/ReadMeAssertions.cs b/Janus.Tests.EndToEnd/ReadMeAssertions.cs
similarity index 78%
rename from UnionsGenerator.EndToEnd.Tests/ReadMeAssertions.cs
rename to Janus.Tests.EndToEnd/ReadMeAssertions.cs
index 3c43d63..9a1b755 100644
--- a/UnionsGenerator.EndToEnd.Tests/ReadMeAssertions.cs
+++ b/Janus.Tests.EndToEnd/ReadMeAssertions.cs
@@ -3,21 +3,17 @@
#pragma warning disable IDE0250
#pragma warning disable IDE0059
#pragma warning disable CS1591
-namespace RhoMicro.CodeAnalysis.UnionsGenerator.EndToEnd.Tests;
+namespace RhoMicro.CodeAnalysis.Janus.EndToEnd.Tests;
+
using System;
using System.Collections.Generic;
-using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Text;
-using System.Threading.Tasks;
-
-using Microsoft.VisualBasic;
public partial class ReadMeAssertions
{
[UnionType]
[UnionType]
private partial struct IntOrString;
+
[Fact]
public void TypeDeclarationTarget()
{
@@ -26,6 +22,7 @@ public void TypeDeclarationTarget()
}
private partial struct GenericUnion<[UnionType] T0, [UnionType] T1>;
+
[Fact]
public void TypeParameterTarget()
{
@@ -33,24 +30,27 @@ public void TypeParameterTarget()
u = GenericUnion.Create(32);
}
- [UnionType>(Alias = "MultipleNames")]
- [UnionType(Alias = "SingleName")]
- private partial struct Names;
+ [UnionType>(Name = "MultipleNames")]
+ [UnionType(Name = "SingleName")]
+ private sealed partial class Names;
+
[Fact]
public void AliasExample()
{
Names n = "John";
- if(n.IsSingleName)
+ if (n.IsSingleName)
{
var singleName = n.AsSingleName;
- } else if(n.IsMultipleNames)
+ }
+ else if (n.IsMultipleNames)
{
var multipleNames = n.AsMultipleNames;
}
}
- [UnionType(Options = UnionTypeOptions.ImplicitConversionIfSolitary)]
+ [UnionType]
private partial struct Int32Alias;
+
[Fact]
public void Solitary()
{
@@ -60,6 +60,7 @@ public void Solitary()
}
private partial struct GenericConvertableUnion<[UnionType] T>;
+
[Fact]
public void SupersetOfParameter()
{
@@ -68,9 +69,10 @@ public void SupersetOfParameter()
}
#pragma warning disable CS8604 // Possible null reference argument.
- [UnionType(Options = UnionTypeOptions.Nullable)]
+ [UnionType(IsNullable = true)]
[UnionType>]
private partial struct NullableStringUnion;
+
[Fact]
public void NullableUnion()
{
@@ -84,27 +86,28 @@ public void NullableUnion()
[UnionType(Groups = ["Number"])]
[UnionType(Groups = ["Text"])]
private partial struct GroupedUnion;
+
[Fact]
public void GroupedUnions()
{
GroupedUnion u = "Hello, World!";
- if(u.IsNumberGroup)
+ if (u.Variant.Group.ContainsNumber)
{
Assert.Fail("Expected union to be text.");
}
- if(!u.IsTextGroup)
+ if (!u.Variant.Group.ContainsText)
{
Assert.Fail("Expected union to be text.");
}
u = 32f;
- if(!u.IsNumberGroup)
+ if (!u.Variant.Group.ContainsNumber)
{
Assert.Fail("Expected union to be number.");
}
- if(u.IsTextGroup)
+ if (u.Variant.Group.ContainsText)
{
Assert.Fail("Expected union to be number.");
}
diff --git a/UnionsGenerator.EndToEnd.Tests/RelatedTypeConversionTests.cs b/Janus.Tests.EndToEnd/RelatedTypeConversionTests.cs
similarity index 76%
rename from UnionsGenerator.EndToEnd.Tests/RelatedTypeConversionTests.cs
rename to Janus.Tests.EndToEnd/RelatedTypeConversionTests.cs
index 341dc0c..f0b8bdf 100644
--- a/UnionsGenerator.EndToEnd.Tests/RelatedTypeConversionTests.cs
+++ b/Janus.Tests.EndToEnd/RelatedTypeConversionTests.cs
@@ -3,7 +3,7 @@
#pragma warning disable CA1305 // Specify IFormatProvider
#pragma warning disable IDE0059 // Unnecessary assignment of a value
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
-namespace RhoMicro.CodeAnalysis.UnionsGenerator.EndToEnd.Tests;
+namespace RhoMicro.CodeAnalysis.Janus.EndToEnd.Tests;
using System;
@@ -12,10 +12,6 @@ public partial class RelatedTypeConversionTests
[UnionType]
[UnionType]
[UnionType]
- [Relation]
- [Relation]
- [Relation]
- [Relation]
private readonly partial struct Union;
[UnionType]
@@ -27,15 +23,16 @@ private sealed partial class CongruentUnion;
public void StringUnionToCongruent()
{
Union u = "Hello, World!";
- CongruentUnion cu = u;
+ var cu = CongruentUnion.Create(u);
Assert.Equal(u.AsString, cu.AsString);
}
+
[Fact]
public void StringCongruentToUnion()
{
CongruentUnion cu = "Hello, World!";
- Union u = cu;
+ var u = Union.Create(cu);
Assert.Equal(cu.AsString, u.AsString);
}
@@ -44,15 +41,16 @@ public void StringCongruentToUnion()
public void DoubleUnionToCongruent()
{
Union u = 32d;
- CongruentUnion cu = u;
+ var cu = CongruentUnion.Create(u);
Assert.Equal(u.AsDouble, cu.AsDouble);
}
+
[Fact]
public void DoubleCongruentToUnion()
{
CongruentUnion cu = 32d;
- Union u = cu;
+ var u = Union.Create(cu);
Assert.Equal(cu.AsDouble, u.AsDouble);
}
@@ -61,36 +59,38 @@ public void DoubleCongruentToUnion()
public void DateTimeUnionToCongruent()
{
Union u = DateTime.Parse("01/10/2009 7:34");
- CongruentUnion cu = u;
+ var cu = CongruentUnion.Create(u);
Assert.Equal(u.AsDateTime, cu.AsDateTime);
}
+
[Fact]
public void DateTimeCongruentToUnion()
{
CongruentUnion cu = DateTime.Parse("01/10/2009 7:34");
- Union u = cu;
+ var u = Union.Create(cu);
Assert.Equal(cu.AsDateTime, u.AsDateTime);
}
[UnionType]
[UnionType]
- private partial class SubsetUnion;
+ private sealed partial class SubsetUnion;
[Fact]
public void StringUnionToSubset()
{
Union u = "Hello, World!";
- var su = (SubsetUnion)u;
+ var su = SubsetUnion.Create(u);
Assert.Equal(u.AsString, su.AsString);
}
+
[Fact]
public void StringSubsetToUnion()
{
SubsetUnion su = "Hello, World!";
- Union u = su;
+ var u = Union.Create(su);
Assert.Equal(su.AsString, u.AsString);
}
@@ -99,22 +99,23 @@ public void StringSubsetToUnion()
public void DoubleUnionToSubset()
{
Union u = 32d;
- _ = Assert.Throws(() => (SubsetUnion)u);
+ _ = Assert.Throws(() => SubsetUnion.Create(u));
}
[Fact]
public void DateTimeUnionToSubset()
{
Union u = DateTime.Parse("01/10/2009 7:34");
- var su = (SubsetUnion)u;
+ var su = SubsetUnion.Create(u);
Assert.Equal(u.AsDateTime, su.AsDateTime);
}
+
[Fact]
public void DateTimeSubsetToUnion()
{
SubsetUnion su = DateTime.Parse("01/10/2009 7:34");
- Union u = su;
+ var u = Union.Create(su);
Assert.Equal(su.AsDateTime, u.AsDateTime);
}
@@ -129,15 +130,16 @@ public void DateTimeSubsetToUnion()
public void StringUnionToSuperset()
{
Union u = "Hello, World!";
- SupersetUnion su = u;
+ var su = SupersetUnion.Create(u);
Assert.Equal(u.AsString, su.AsString);
}
+
[Fact]
public void StringSupersetToUnion()
{
SupersetUnion su = "Hello, World!";
- var u = (Union)su;
+ var u = Union.Create(su);
Assert.Equal(su.AsString, u.AsString);
}
@@ -146,22 +148,23 @@ public void StringSupersetToUnion()
public void Int32SupersetToUnion()
{
SupersetUnion su = 32;
- _ = Assert.Throws(() => (Union)su);
+ _ = Assert.Throws(() => Union.Create(su));
}
[Fact]
public void DateTimeUnionToSuperset()
{
Union u = DateTime.Parse("01/10/2009 7:34");
- SupersetUnion su = u;
+ var su = SupersetUnion.Create(u);
Assert.Equal(u.AsDateTime, su.AsDateTime);
}
+
[Fact]
public void DateTimeSupersetToUnion()
{
SupersetUnion su = DateTime.Parse("01/10/2009 7:34");
- var u = (Union)su;
+ var u = Union.Create(su);
Assert.Equal(su.AsDateTime, u.AsDateTime);
}
@@ -170,15 +173,16 @@ public void DateTimeSupersetToUnion()
public void DoubleUnionToSuperset()
{
Union u = 32d;
- SupersetUnion su = u;
+ var su = SupersetUnion.Create(u);
Assert.Equal(u.AsDouble, su.AsDouble);
}
+
[Fact]
public void DoubleSupersetToUnion()
{
SupersetUnion su = 32d;
- var u = (Union)su;
+ var u = Union.Create(su);
Assert.Equal(su.AsDouble, u.AsDouble);
}
@@ -187,41 +191,43 @@ public void DoubleSupersetToUnion()
[UnionType]
[UnionType]
[UnionType>]
- private partial class IntersectionUnion;
+ private sealed partial class IntersectionUnion;
[Fact]
public void Int16IntersectionToUnion()
{
IntersectionUnion iu = 32;
- _ = Assert.Throws(() => (Union)iu);
+ _ = Assert.Throws(() => Union.Create(iu));
}
+
[Fact]
public void ListIntersectionToUnion()
{
IntersectionUnion iu = new List();
- _ = Assert.Throws(() => (Union)iu);
+ _ = Assert.Throws(() => Union.Create(iu));
}
[Fact]
public void DateTimeUnionToIntersection()
{
Union iu = DateTime.Parse("01/10/2009 7:34");
- _ = Assert.Throws(() => (IntersectionUnion)iu);
+ _ = Assert.Throws(() => IntersectionUnion.Create(iu));
}
[Fact]
public void StringUnionToIntersection()
{
Union u = "Hello, World!";
- var iu = (IntersectionUnion)u;
+ var iu = IntersectionUnion.Create(u);
Assert.Equal(u.AsString, iu.AsString);
}
+
[Fact]
public void StringIntersectionToUnion()
{
IntersectionUnion iu = "Hello, World!";
- var u = (Union)iu;
+ var u = Union.Create(iu);
Assert.Equal(iu.AsString, u.AsString);
}
@@ -230,15 +236,16 @@ public void StringIntersectionToUnion()
public void DoubleUnionToIntersection()
{
Union u = 32d;
- var iu = (IntersectionUnion)u;
+ var iu = IntersectionUnion.Create(u);
Assert.Equal(u.AsDouble, iu.AsDouble);
}
+
[Fact]
public void DoubleIntersectionToUnion()
{
IntersectionUnion iu = 32d;
- var u = (Union)iu;
+ var u = Union.Create(iu);
Assert.Equal(iu.AsDouble, u.AsDouble);
}
diff --git a/UnionsGenerator.EndToEnd.Tests/RepresentableTypeConversionTests.cs b/Janus.Tests.EndToEnd/RepresentableTypeConversionTests.cs
similarity index 80%
rename from UnionsGenerator.EndToEnd.Tests/RepresentableTypeConversionTests.cs
rename to Janus.Tests.EndToEnd/RepresentableTypeConversionTests.cs
index 334fbc4..70a80d7 100644
--- a/UnionsGenerator.EndToEnd.Tests/RepresentableTypeConversionTests.cs
+++ b/Janus.Tests.EndToEnd/RepresentableTypeConversionTests.cs
@@ -2,15 +2,15 @@
#pragma warning disable IDE0059 // Unnecessary assignment of a value
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
-namespace RhoMicro.CodeAnalysis.UnionsGenerator.EndToEnd.Tests;
+namespace RhoMicro.CodeAnalysis.Janus.EndToEnd.Tests;
using System;
using System.Numerics;
public partial class RepresentableTypeConversionTests
{
- [UnionType(Alias = "ErrorMessage")]
- private readonly partial struct Result<[UnionType(Alias = "Result")] T>;
+ [UnionType(Name = "ErrorMessage")]
+ private readonly partial struct Result<[UnionType(Name = "Result")] T>;
[Fact]
public void IsImplicitlyConvertibleFromString()
@@ -40,19 +40,19 @@ public void IsExplicitlyConvertibleToString()
public void IsNotExplicitlyConvertibleToString()
{
Result r = 32;
- _ = Assert.Throws(() => (String)r);
+ _ = Assert.Throws(() => (String)r);
}
[Fact]
public void IsNotExplicitlyConvertibleToInt32()
{
Result