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 r = "Hello, World!"; - _ = Assert.Throws(() => (Int32)r); + _ = Assert.Throws(() => (Int32)r); } [Fact] public void IsNotExplicitlyConvertibleToChar() { Result r = "Hello, World!"; - _ = Assert.Throws(() => (Char)r); + _ = Assert.Throws(() => (Char)r); } [Fact] public void IsExplicitlyConvertibleToInt32() diff --git a/Janus.Tests.EndToEnd/ToStringTests.cs b/Janus.Tests.EndToEnd/ToStringTests.cs new file mode 100644 index 0000000..b6e03a2 --- /dev/null +++ b/Janus.Tests.EndToEnd/ToStringTests.cs @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MPL-2.0 + +namespace RhoMicro.CodeAnalysis.Janus.EndToEnd.Tests; +using System; +using System.Collections.Immutable; + +public partial class ToStringTests +{ + [UnionType] + [UnionTypeSettings(ToStringSetting = ToStringSetting.None)] + private sealed partial class NoToStringUnionType; + + [Fact] + public void UsesDefaultToString() + { + NoToStringUnionType u = "Foo"; + var expected = typeof(NoToStringUnionType).FullName; + var actual = u.ToString(); + + Assert.Equal(expected, actual); + } + + [UnionType] + [UnionTypeSettings(ToStringSetting = ToStringSetting.Simple)] + private sealed partial class SimpleToStringUnionType; + [Fact] + public void UsesSimpleToString() + { + const String expected = "Foo"; + SimpleToStringUnionType u = expected; + var actual = u.ToString(); + + Assert.Equal(expected, actual); + } + + [UnionType, string>] + [UnionTypeSettings(JsonConverterSetting = JsonConverterSetting.EmitJsonConverter)] + sealed partial class IntDoubleString; + + [Fact] + public void UsesDetailedToString2() + { + const String expected = "IntDoubleString { Variants: [Double, , ImmutableArray, String], Value: 42 }"; + IntDoubleString u = 42; + var actual = u.ToString(); + + Assert.Equal(expected, actual); + } + + [UnionType] + [UnionTypeSettings(ToStringSetting = ToStringSetting.Detailed)] + private sealed partial class DetailedToStringUnionType; + + [Fact] + public void UsesDetailedToString1() + { + const String expected = "DetailedToStringUnionType { Variants: [Int32, ], Value: Foo }"; + DetailedToStringUnionType u = "Foo"; + var actual = u.ToString(); + + Assert.Equal(expected, actual); + } + + [UnionType] + private sealed partial class CustomToStringUnionType + { + public override String ToString() => "Foo"; + } + + [Fact] + public void UsesCustomToString() + { + CustomToStringUnionType u = "Bar"; + var expected = "Foo"; + var actual = u.ToString(); + + Assert.Equal(expected, actual); + } +} diff --git a/UnionsGenerator.EndToEnd.Tests/TryAsFunctionsTests.cs b/Janus.Tests.EndToEnd/TryAsFunctionsTests.cs similarity index 75% rename from UnionsGenerator.EndToEnd.Tests/TryAsFunctionsTests.cs rename to Janus.Tests.EndToEnd/TryAsFunctionsTests.cs index 432dbbb..a05e142 100644 --- a/UnionsGenerator.EndToEnd.Tests/TryAsFunctionsTests.cs +++ b/Janus.Tests.EndToEnd/TryAsFunctionsTests.cs @@ -2,22 +2,22 @@ #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 TryAsFunctionsTests { - [UnionType(Alias = "Int")] + [UnionType(Name = "Int")] [UnionType>] - private partial class Union<[UnionType] T>; + private sealed partial class Union<[UnionType] T>; [Fact] public void IsIntWhenRepresentingInt32() { const Int32 expected = 32; Union u = expected; - Assert.True(u.TryAsInt(out var actual)); + Assert.True(u.TryCastToInt(out var actual)); Assert.Equal(expected, actual); } [Fact] @@ -25,7 +25,7 @@ public void IsNotIntWhenRepresentingList() { const Int32 expected = 0; Union u = new List(); - Assert.False(u.TryAsInt(out var actual)); + Assert.False(u.TryCastToInt(out var actual)); Assert.Equal(expected, actual); } [Fact] @@ -33,7 +33,7 @@ public void IsNotIntWhenRepresentingByte() { const Int32 expected = 0; Union u = (Byte)32; - Assert.False(u.TryAsInt(out var actual)); + Assert.False(u.TryCastToInt(out var actual)); Assert.Equal(expected, actual); } @@ -42,7 +42,7 @@ public void IsNotListWhenRepresentingInt32() { List? expected = null; Union u = 32; - Assert.False(u.TryAsList_of_String(out var actual)); + Assert.False(u.TryCastToList(out var actual)); Assert.Equal(expected, actual); } [Fact] @@ -50,7 +50,7 @@ public void IsListWhenRepresentingList() { var expected = new List(); Union u = expected; - Assert.True(u.TryAsList_of_String(out var actual)); + Assert.True(u.TryCastToList(out var actual)); Assert.Equal(expected, actual); } [Fact] @@ -58,7 +58,7 @@ public void IsNotListWhenRepresentingByte() { List? expected = null; Union u = (Byte)32; - Assert.False(u.TryAsList_of_String(out var actual)); + Assert.False(u.TryCastToList(out var actual)); Assert.Equal(expected, actual); } @@ -67,7 +67,7 @@ public void IsNotByteWhenRepresentingInt32() { Byte expected = 0; Union u = 32; - Assert.False(u.TryAsT(out var actual)); + Assert.False(u.TryCastToT(out var actual)); Assert.Equal(expected, actual); } [Fact] @@ -75,7 +75,7 @@ public void IsNotByteWhenRepresentingList() { Byte expected = 0; Union u = new List(); - Assert.False(u.TryAsT(out var actual)); + Assert.False(u.TryCastToT(out var actual)); Assert.Equal(expected, actual); } [Fact] @@ -83,7 +83,7 @@ public void IsByteWhenRepresentingByte() { Byte expected = 32; Union u = expected; - Assert.True(u.TryAsT(out var actual)); + Assert.True(u.TryCastToT(out var actual)); Assert.Equal(expected, actual); } } diff --git a/Janus.Tests/Janus.Tests.csproj b/Janus.Tests/Janus.Tests.csproj new file mode 100644 index 0000000..6685fb7 --- /dev/null +++ b/Janus.Tests/Janus.Tests.csproj @@ -0,0 +1,42 @@ + + + + enable + enable + Exe + Janus.Tests + net10.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Janus.Tests/JanusAnalyzerTests.cs b/Janus.Tests/JanusAnalyzerTests.cs new file mode 100644 index 0000000..1f4f82b --- /dev/null +++ b/Janus.Tests/JanusAnalyzerTests.cs @@ -0,0 +1,449 @@ +// SPDX-License-Identifier: MPL-2.0 + +namespace Janus.Tests; + +using Microsoft.CodeAnalysis.Testing; +using RhoMicro.CodeAnalysis.Janus; + +public class JanusAnalyzerTests +{ + [Fact] + public Task EqualsIsNotOverriddenIfUserProvided() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + [UnionType] + partial struct Union + { + public bool Equals(Union other) => false; + } + """); + + [Fact] + public Task StructUnionHasEqualityOperatorEmitted() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + [UnionType] + partial struct Union + { + public static bool operator !=(Union a, Union b) => throw null; + public static bool operator ==(Union a, Union b) => throw null; + } + """); + + [Theory] + [InlineData("None")] + [InlineData("Simple")] + [InlineData("Detailed")] + public Task ToStringSettingIgnored(String setting) => JanusTest.TestAnalyzer( + $$""" + using RhoMicro.CodeAnalysis; + + [UnionType, UnionTypeSettings( {|RMJ0001:ToStringSetting = ToStringSetting.{{setting}}|} )] + partial struct Union + { + public override string ToString() => throw null; + } + """); + + [Theory] + [InlineData("class ")] + [InlineData("struct ")] + [InlineData("")] + public Task RecordUnionsAreDisallowed(String modifier) => JanusTest.TestAnalyzer( + $$""" + using RhoMicro.CodeAnalysis; + + #pragma warning disable RMJ0018 + + [UnionType] + partial {|RMJ0002:record|} {{modifier}}Union; + """); + + [Fact] + public Task ExplicitlyGenericUnionsCannotBeJsonSerializable() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + [UnionTypeSettings( {|RMJ0003:JsonConverterSetting = JsonConverterSetting.EmitJsonConverter|} )] + partial struct Union<[UnionType] T>; + """); + + [Fact] + public Task TransientlyGenericUnionsCannotBeJsonSerializable() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + partial class Outer + { + [UnionType] + [UnionTypeSettings( {|RMJ0003:JsonConverterSetting = JsonConverterSetting.EmitJsonConverter|} )] + partial struct Union; + } + """); + + [Fact] + public Task ExplicitlyGenericConstValueUnionsCannotBeJsonSerializable() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + static class Constants + { + public const JsonConverterSetting Value = JsonConverterSetting.EmitJsonConverter; + } + + [UnionTypeSettings( {|RMJ0003:JsonConverterSetting = Constants.Value|} )] + partial struct Union<[UnionType] T>; + """); + + [Fact] + public Task TransientlyGenericConstValueUnionsCannotBeJsonSerializable() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + static class Constants + { + public const JsonConverterSetting Value = JsonConverterSetting.EmitJsonConverter; + } + + partial class Outer + { + [UnionType] + [UnionTypeSettings( {|RMJ0003:JsonConverterSetting = Constants.Value|} )] + partial struct Union; + } + """); + + [Fact] + public Task NoMoreThan31VariantGroupsMayBeDefined() => JanusTest.TestAnalyzer( + $$""" + using RhoMicro.CodeAnalysis; + + [UnionType({|RMJ0004:Groups = [{{String.Join(", ", Enumerable.Range(1, 32).Select(i => $"\"Group{i}\""))}}]|})] + partial struct Union; + """); + + [Fact] + public Task StaticUnionsAreDisallowed() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + #pragma warning disable RMJ0018 + + [UnionType] + {|RMJ0005:static|} partial class Union; + """); + + [Fact] + public Task VariantNamesMustBeUnique_SingleExplicitDuplicate() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + [UnionType({|RMJ0006:Name = "Foo"|})] + [UnionType({|RMJ0006:Name = "Foo"|})] + partial struct Union; + """); + + [Fact] + public Task VariantNamesMustBeUnique_MultipleExplicitDuplicate() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + [UnionType({|RMJ0006:Name = "Foo"|})] + [UnionType({|RMJ0006:Name = "Foo"|})] + [UnionType({|RMJ0006:Name = "Foo"|})] + [UnionType({|RMJ0006:Name = "Foo"|})] + partial struct Union; + """); + + [Fact] + public Task VariantNamesMustBeUnique_SingleImplicitDuplicate() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + using System.Collections.Generic; + + [UnionType<{|RMJ0006:List|}>] + [UnionType<{|RMJ0006:List|}>] + [UnionType] + partial struct Union; + """); + + [Fact] + public Task VariantNamesMustBeUnique_SingleImplicitInlineDuplicate() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + using System.Collections.Generic; + + [UnionType<{|RMJ0006:List|}, int, {|RMJ0006:List|}>] + partial struct Union; + """); + + [Fact] + public Task VariantNamesMustBeUnique_MultipleImplicitDuplicate() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + using System.Collections.Generic; + + [UnionType] + [UnionType<{|RMJ0006:List|}>] + [UnionType<{|RMJ0006:List|}>] + [UnionType<{|RMJ0006:List|}>] + partial struct Union; + """); + + [Fact] + public Task VariantNamesMustBeUnique_TypeParameter() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + using System.Collections.Generic; + + [UnionType<{|RMJ0006:System.String|}>] + partial struct Union<[UnionType]{|RMJ0006:String|}>; + """); + + [Fact] + public Task EnsureValidStructUnionState() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + [UnionType] + partial struct {|RMJ0007:Union|}; + """); + + [Fact] + public Task InterfaceVariantIsExcludedFromConversionOperators() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + using System.Collections.Generic; + + #pragma warning disable RMJ0018 + + [UnionType<{|RMJ0008:IEnumerable|}>] + partial class Union; + """ + ); + + [Fact] + public Task VariantTypesMustBeUnique() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + #pragma warning disable RMJ0018 + #pragma warning disable RMJ0006 + + [UnionType<{|RMJ0009:int|}>] + [UnionType<{|RMJ0009:int|}>] + partial class Union; + """ + ); + + [Fact] + public Task VariantTypesMustBeUniqueInline() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + #pragma warning disable RMJ0018 + #pragma warning disable RMJ0006 + + [UnionType<{|RMJ0009:int|}, {|RMJ0009:int|}>] + partial class Union; + """ + ); + + [Fact] + public Task ObjectCannotBeUsedAsAVariant_Class() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + #pragma warning disable RMJ0018 + + [UnionType<{|RMJ0010:object|}>] + partial class Union; + """ + ); + + [Fact] + public Task ObjectCannotBeUsedAsAVariant_Struct() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + #pragma warning disable RMJ0007 + + [UnionType<{|RMJ0010:object|}>] + partial struct Union; + """ + ); + + [Fact] + public Task ValueTypeCannotBeUsedAsAVariantOfStructUnions() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + #pragma warning disable RMJ0007 + + [UnionType<{|RMJ0019:System.ValueType|}>] + partial struct Union; + """ + ); + + [Fact] + public Task UnionCannotBeUsedAsVariantOfItself() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + #pragma warning disable RMJ0018 + + [UnionType<{|RMJ0012:Union|}>] + partial class Union; + """ + ); + + [Theory] + [InlineData("System.Collections.Generic.HashSet")] + [InlineData("System.Collections.Generic.List")] + public Task UnionCannotExplicitlyDefineBaseType(String baseType) => JanusTest.TestAnalyzer( + $$""" + using RhoMicro.CodeAnalysis; + + #pragma warning disable RMJ0018 + + [UnionType] + partial class {|RMJ0013:Union|} : {{baseType}}; + """ + ); + + [Theory] + [InlineData( + "System.Collections.Generic.IEnumerable", + """ + public System.Collections.Generic.IEnumerator GetEnumerator() => throw null; + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => throw null; + """ + )] + [InlineData( + "System.IComparable", + """ + public int CompareTo(Union other) => throw null; + """ + )] + public Task UnionCanExplicitlyDefineInterfaces(String baseType, String implementation) => JanusTest.TestAnalyzer( + $$""" + using RhoMicro.CodeAnalysis; + + #pragma warning disable RMJ0018 + + [UnionType] + partial class Union : {{baseType}} + { + #pragma warning disable + {{implementation}} + #pragma warning restore + } + """ + ); + + [Fact] + public Task PreferNullableStructOverIsNullable() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + #pragma warning disable RMJ0018 + + [UnionType({|RMJ0014:IsNullable = true|})] + [UnionType(IsNullable = true)] + partial class Union; + """ + ); + + [Fact] + public Task NullableVariantNotAllowedAlongWithNonNullableVariant() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + #pragma warning disable RMJ0018 + + [UnionType<{|RMJ0015:int|}>] + [UnionType<{|RMJ0015:System.Nullable|}>(Name = "NullableInt")] + partial class Union; + """ + ); + + [Fact] + public Task UnionTypeSettingsAttributeIgnoredDueToMissingUnionTypeAttribute() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + [{|RMJ0016:UnionTypeSettings|}] + partial class Union; + """ + ); + + [Fact] + public Task DuplicateVariantGroupNamesAreIgnored() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + #pragma warning disable RMJ0018 + + [UnionType(Groups = [ "Group", {|RMJ0017:"Group"|} ])] + partial class Union; + """ + ); + + [Fact] + public Task ClassUnionsShouldBeSealed() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + [UnionType] + partial class {|RMJ0018:Union|}; + """); + + [Fact] + public Task UnionCannotBeRefStruct() => JanusTest.TestAnalyzer( + """ + using RhoMicro.CodeAnalysis; + + [UnionType] + ref partial struct {|RMJ0020:Union|}; + """); + + [Theory] + [InlineData(""" + using RhoMicro.CodeAnalysis; + using System.Collections.Generic; + + [UnionType(Name = "Int")] + [UnionType>] + sealed partial class Union<[UnionType(Name = "ValueT")] T> where T : struct + { + public void Foo() + { + Switch( + onInt: _ => { }, + onList: _ => { }, + onValueT: _ => { }); + } + } + """ + )] + [InlineData( + """ + using RhoMicro.CodeAnalysis; + + [UnionType] + [UnionTypeSettings(ToStringSetting = ToStringSetting.Simple)] + sealed partial class Union; + """ + )] + [InlineData( + """ + using RhoMicro.CodeAnalysis; + + [UnionType] + [UnionTypeSettings(ToStringSetting = ToStringSetting.None)] + sealed partial class Union; + """ + )] + public Task ProducesNoDiagnostics(String source) => JanusTest.TestAnalyzer(source); +} diff --git a/Janus.Tests/JanusCodeFixProviderTests.cs b/Janus.Tests/JanusCodeFixProviderTests.cs new file mode 100644 index 0000000..e9016c8 --- /dev/null +++ b/Janus.Tests/JanusCodeFixProviderTests.cs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MPL-2.0 + +namespace Janus.Tests; + +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; +using RhoMicro.CodeAnalysis.Janus; + +public class JanusCodeFixProviderTests +{ + [Fact] + public Task ClassUnionsShouldBeSealed() => + JanusTest.TestCodeFix( + source: + """ + using RhoMicro.CodeAnalysis; + + [UnionType] + partial class {|RMJ0018:Union|}; + """, + fixedSource: + """ + using RhoMicro.CodeAnalysis; + + [UnionType] + sealed partial class Union; + """); +} diff --git a/Janus.Tests/JanusTest.cs b/Janus.Tests/JanusTest.cs new file mode 100644 index 0000000..9d82255 --- /dev/null +++ b/Janus.Tests/JanusTest.cs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MPL-2.0 + +namespace Janus.Tests; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using RhoMicro.CodeAnalysis; +using RhoMicro.CodeAnalysis.Janus; + +class JanusTest : CSharpCodeFixTest +{ + public JanusTest() + { + TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck; + ReferenceAssemblies = ReferenceAssemblies.Net.Net90; + TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(IUnion).Assembly.Location)); + } + + public static Task TestCodeFix(String source, String fixedSource) + { + return CodeFixVerifier + .VerifyCodeFixAsync(source, fixedSource) + .WaitAsync(TestContext.Current.CancellationToken); + } + + public static Task TestAnalyzer(String source, params DiagnosticResult[] expected) + { + return CodeFixVerifier + .VerifyAnalyzerAsync(source, expected) + .WaitAsync(TestContext.Current.CancellationToken); + } + + protected override IEnumerable GetSourceGenerators() => + [ + typeof(JanusGenerator), + typeof(OverloadResolutionPriorityAttributeGenerator) // TODO: remove when net10 is supported by MS.CA.Testing.ReferenceAssemblies + ]; +} diff --git a/Janus.Tests/OverloadResolutionPriorityAttributeGenerator.cs b/Janus.Tests/OverloadResolutionPriorityAttributeGenerator.cs new file mode 100644 index 0000000..8ca461c --- /dev/null +++ b/Janus.Tests/OverloadResolutionPriorityAttributeGenerator.cs @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MPL-2.0 + +namespace Janus.Tests; + +using Microsoft.CodeAnalysis; + +sealed class OverloadResolutionPriorityAttributeGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) => + context.RegisterPostInitializationOutput(ctx => + ctx.AddSource("System.Runtime.CompilerServices.OverloadResolutionPriorityAttribute", + """ + #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.Runtime.CompilerServices + { + /// + /// Specifies the priority of a member in overload resolution. When unspecified, the default priority is 0. + /// + [global::System.AttributeUsage( + global::System.AttributeTargets.Method | + global::System.AttributeTargets.Constructor | + global::System.AttributeTargets.Property, + AllowMultiple = false, + Inherited = false)] + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + internal sealed class OverloadResolutionPriorityAttribute : global::System.Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The priority of the attributed member. Higher numbers are prioritized, lower numbers are deprioritized. 0 is the default if no attribute is present. + public OverloadResolutionPriorityAttribute(int priority) + { + Priority = priority; + } + + /// + /// The priority of the member. + /// + public int Priority { get; } + } + } + + """ + )); +} diff --git a/Janus.Tests/xunit.runner.json b/Janus.Tests/xunit.runner.json new file mode 100644 index 0000000..86c7ea0 --- /dev/null +++ b/Janus.Tests/xunit.runner.json @@ -0,0 +1,3 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json" +} diff --git a/Janus/Janus.csproj b/Janus/Janus.csproj new file mode 100644 index 0000000..a79aee6 --- /dev/null +++ b/Janus/Janus.csproj @@ -0,0 +1,35 @@ + + + + + netstandard2.0 + false + + + + true + true + + Generate hybrid (tagged/type) union types. + + Source Generator; Union Types; Unions;Discriminated Unions;Tagged Unions;Variant Types + https://raw.githubusercontent.com/PaulBraetz/RhoMicro.CodeAnalysis/master/Janus/PackageLogo.svg + + + + + + + + + + + + + + + + + + + diff --git a/UnionsGenerator/Logo_Explorations.odg b/Janus/Logo_Explorations.odg similarity index 100% rename from UnionsGenerator/Logo_Explorations.odg rename to Janus/Logo_Explorations.odg diff --git a/UnionsGenerator/PackageLogo.svg b/Janus/PackageLogo.svg similarity index 100% rename from UnionsGenerator/PackageLogo.svg rename to Janus/PackageLogo.svg diff --git a/UnionsGenerator/README.md b/Janus/README.md similarity index 100% rename from UnionsGenerator/README.md rename to Janus/README.md diff --git a/UnionsGenerator/ReadmeLogo.svg b/Janus/ReadmeLogo.svg similarity index 100% rename from UnionsGenerator/ReadmeLogo.svg rename to Janus/ReadmeLogo.svg diff --git a/Janus/requirements.md b/Janus/requirements.md new file mode 100644 index 0000000..5b87b72 --- /dev/null +++ b/Janus/requirements.md @@ -0,0 +1,123 @@ +# New UnionsGenerator Design + +## Terminology + +The term `union` refers to a tagged union or sum type, or an instance thereof. +It is able to represent an instance of a type present in its set of representable types. + +The term `Union` refers to a nonspecific generated type implementing the defined union. + +The term `variant` refers to a type representable by a union. +Unless improperly initialized, a union is always representing an instance of one of its variants. + +The term `value` refers to the variant instance being represented by a union. + +The term `adapter` refers to the type adapting the union onto interfaces implemented by all its variants. + +## Requirements + +| id | description | issues | met (×/✓) | +|----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------|-----------| +| 1 | An error is issued if `Union` is not partial. | #151 | × | +| 2 | Interfaces implemented by all variants are implemented by an adapter property on the union. | #40 | × | +| 3 | Conflicting interface member implementations on the adapter are implemented explicitly. | #40 | × | +| 4 | An error is issued for `ref` like variants. | | × | +| 4 | An error is issued for `record` unions. | | × | +| 5 | A warning is issued for non-nullable reference variant on struct union. | | × | +| 6 | An error is issued if more than 31 variant groups are defined. This is to allow groups to be modelled using a [Flags] enum. | | × | +| 7 | Variant group names have diagnostics mapped onto their definition in the attribute usage. | | × | +| 8 | Variant names have diagnostics mapped onto their definition in the attribute usage. | | × | +| 9 | The backing type of the `VariantKind` is chosen as the smallest integral datatype able to accommodate all variants. | | × | +| 10 | An option is provided to box managed struct variants. | | × | +| 11 | Managed struct variants are stored in dedicated fields by default. | | × | +| 12 | An error is issued for static variants. | | × | +| 13 | An error is issued for conflicting variant names. | | × | +| 14 | An error is issued if no unmanaged, managed struct or nullable reference variant is defined for a struct union. This is to prevent struct unions to be left in an invalid state when uninitialized. | | × | +| 15 | Variants are ordered by unmanaged, managed struct, nullable reference, reference, fully qualified type name. This is to force the default variant to be valid when the struct union is uninitialized. | | × | +| 16 | If exactly one variant is defined, an implicit conversion to that variant is defined. | | × | +| 17 | If multiple variants are defined, an explicit conversion to each variant is defined. | | × | +| 18 | If a validation method is implemented, then an explicit conversion from the validated variant is defined. | | × | +| 19 | If a validation method is not implemented, then an implicit conversion from the variant is defined. | | × | +| 20 | Documentation comments are emitted for every generated member. | | × | +| 21 | Parameter lists are wrapped and indented. | | × | +| 22 | The backing type of the `VariantGroupKinds` is chosen as the smallest integral datatype able to accommodate all variants groups. | | × | +| 23 | `Union` methods are grouped by the variant they are specific to. | | × | +| 24 | Variant group names are ordered alphabetically, except for the default group `None`, which must always be the first element. | | × | +| 25 | An error is issued for json serializable unions that are generic (transitively). | | × | +| 26 | An error is issued for interface variants. | | × | +| 27 | An error is issued for duplicate variant types. | | × | +| 28 | An error is issued for static unions. | | × | +| 29 | An error is issued if the union implements the variant. | | × | +| 30 | An error is issued for `allows ref` variants. | | × | +| 31 | /* A warning is issued for transitively generic unions. */ | | × | +| 32 | An error is issued for variants that are the union itself. | | × | +| 33 | An error is issued for any non-interface base type (excluding `System.Object`, `System.ValueType`) implemented by the union. | | × | +| 34 | A warning is issued for `IsNullable = true` set on value type variants. | | × | +| 35 | A error is issued if a variants of `Nullable` and `T` are defined for the same union. | | × | +| 36 | A warning is issued for an explicitly set `ToStringSetting` if a `ToString` implementation is user provided. | | × | +| 37 | If a `ToString` implementation is user provided, no conflicting implementation is emitted. | | × | +| 38 | An error is issued for duplicate variant names. | | × | +| 39 | | | × | +| 40 | | | × | +| 41 | | | × | +| 42 | | | × | +| 43 | | | × | +| 44 | | | × | +| 45 | | | × | +| 46 | | | × | +| 47 | | | × | +| 48 | | | × | +| 49 | | | × | +| 50 | | | × | + +## Breaking Changes + +This is a non-exhaustive list of breaking changes to the previous version: + +| id | change | remediation / replacement | +|----|----------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------| +| 1 | `RelationAttribute` has been removed. | Use explicit conversion through `TTo.Create(TFrom)`, where `TTo` and `TFrom` are unions. | +| 2 | `UnionTypeFactoryAttribute` has been removed. | Custom validation logic should be implemented through the partial `static partial void Validate(TVariant, bool, ref bool)` method. | +| 3 | `LayoutSetting` and `UnionTypeSettingsAttribute.Layout` have been removed. | None - layouts are no longer customizable. | +| 4 | `ConstructorAccessibilitySetting` and `UnionTypeSettingsAttribute.ConstructorAccessibility` have been removed. | None - constructor accessibility is no longer customizable. | +| 5 | `InterfaceMatchSetting` and `UnionTypeSettingsAttribute.InterfaceMatchSetting` have been removed. | Use the `Switch` overloads to match interface implementations. | +| 6 | `DiagnosticsLevelSettings` and `UnionTypeSettingsAttribute.DiagnosticsLevel` have been removed. | Diagnostics should be configured through builtin mechanisms like `.editorconfig` or `#pragma warning disable`. | +| 7 | `MiscellaneousSettings` and `UnionTypeSettingsAttribute.Miscellaneous` have been removed. | Enable json converter generation through the `UnionTypeSettingsAttribute.IsJsonSerializable` property. | +| 8 | `UnionTypeSettingsAttribute.TypeDeclarationPreface` has been removed. | Apply any comment or attribute annotations to your partial declaration of the union. | +| 9 | `Match` methods have been removed. | Use the new `Switch` method overloads. | +| 10 | All naming options from `UnionTypeSettings` have been removed. | None, open an issue on the github repository outlining your custom naming needs. | +| 11 | `StorageOption` and `UnionTypeAttribute.Storage` have been removed. | None, storage is no longer customizable. | +| 12 | `UnionTypeAttribute.FactoryName` has been removed. | None, factory names are no longer customizable. | +| 13 | `UnionTypeOptions` has been removed. | | +| 14 | | | +| 15 | | | +| 16 | | | +| 17 | | | +| 18 | | | +| 19 | | | +| 20 | | | + +- Relations have been removed +- Factories have been removed + +## Notes + +### TODO + +- relations + +### Table Template + +| id | signature | description | conditions | issues | met (×/✓) | +|----|-----------|-------------|------------|--------|-----------| +| 1 | | | | | × | +| 2 | | | | | × | +| 3 | | | | | × | +| 4 | | | | | × | +| 5 | | | | | × | +| 6 | | | | | × | +| 7 | | | | | × | +| 8 | | | | | × | +| 9 | | | | | × | +| 10 | | | | | × | + diff --git a/JsonSchemaGenerator.Cli/JsonSchemaGenerator.Cli.csproj b/JsonSchemaGenerator.Cli/JsonSchemaGenerator.Cli.csproj index 27cec79..5b2f8db 100644 --- a/JsonSchemaGenerator.Cli/JsonSchemaGenerator.Cli.csproj +++ b/JsonSchemaGenerator.Cli/JsonSchemaGenerator.Cli.csproj @@ -13,7 +13,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/JsonSchemaGenerator/JsonSchemaGenerator.csproj b/JsonSchemaGenerator/JsonSchemaGenerator.csproj index 1c8b61a..53b91cc 100644 --- a/JsonSchemaGenerator/JsonSchemaGenerator.csproj +++ b/JsonSchemaGenerator/JsonSchemaGenerator.csproj @@ -29,7 +29,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Lyra/AttributeArgumentComponent.cs b/Lyra/AttributeArgumentComponent.cs index 7b3e596..c5d29fc 100644 --- a/Lyra/AttributeArgumentComponent.cs +++ b/Lyra/AttributeArgumentComponent.cs @@ -19,26 +19,29 @@ namespace RhoMicro.CodeAnalysis.Lyra; { private AttributeArgumentComponent( Char assignmentOperator, - TypeNameComponent? type = null, - ICSharpSourceComponent? expressionComponent = null, - String? expression = null, - String? name = null) + TypeNameComponent? type, + ICSharpSourceComponent? expressionComponent, + String? expression, + String? name, + String? member) { _type = type; _expressionComponent = expressionComponent; _expression = expression; _name = name; _assignmentOperator = assignmentOperator; + _member = member; } private readonly TypeNameComponent? _type; private readonly ICSharpSourceComponent? _expressionComponent; + private readonly String? _member; private readonly String? _expression; private readonly String? _name; private readonly Char _assignmentOperator; /// - /// Creates a positional (constructor) argument of type . + /// Creates a positional (constructor) argument of type using a typeof() expression. /// /// /// The type to append into the typeof() expression assigned to the parameter. @@ -49,12 +52,41 @@ private AttributeArgumentComponent( /// /// A new positional argument component. /// - public static AttributeArgumentComponent CreatePositional(TypeNameComponent type, String? parameterName = null) + public static AttributeArgumentComponent CreatePositional( + TypeNameComponent type, + String? parameterName = null) => new(assignmentOperator: ':', type: type, expressionComponent: null, expression: null, - name: parameterName); + name: parameterName, + member: null); + + /// + /// Creates a positional (constructor) argument using a type.member expression. + /// + /// + /// The type whose member to use. + /// + /// + /// The member to use. + /// + /// + /// The name of the parameter. + /// + /// + /// A new positional argument component. + /// + public static AttributeArgumentComponent CreatePositionalMemberAccess( + TypeNameComponent type, + String member, + String? parameterName = null) + => new(assignmentOperator: ':', + type: type, + expressionComponent: null, + expression: null, + name: parameterName, + member: member); /// /// Creates a positional (constructor) argument. @@ -68,13 +100,15 @@ public static AttributeArgumentComponent CreatePositional(TypeNameComponent type /// /// A new positional argument component. /// - public static AttributeArgumentComponent CreatePositional(ICSharpSourceComponent expressionComponent, - String? parameterName = null) + public static AttributeArgumentComponent CreatePositional( + ICSharpSourceComponent expressionComponent, + String? parameterName = null) => new(assignmentOperator: ':', type: null, expressionComponent: expressionComponent, expression: null, - name: parameterName); + name: parameterName, + member: null); /// /// Creates a positional (constructor) argument. @@ -93,7 +127,8 @@ public static AttributeArgumentComponent CreatePositional(String expression, Str type: null, expressionComponent: null, expression: expression, - name: parameterName); + name: parameterName, + member: null); /// /// Creates a named (property) argument of type . @@ -112,7 +147,34 @@ public static AttributeArgumentComponent CreateNamed(TypeNameComponent type, Str type: type, expressionComponent: null, expression: null, - name: propertyName); + name: propertyName, + member: null); + + /// + /// Creates a named (property) argument using a type.member expression. + /// + /// + /// The type whose member to use. + /// + /// + /// The member to use. + /// + /// + /// The name of the property. + /// + /// + /// A new positional argument component. + /// + public static AttributeArgumentComponent CreateNamedMemberAccess( + TypeNameComponent type, + String member, + String propertyName) + => new(assignmentOperator: '=', + type: type, + expressionComponent: null, + expression: null, + name: propertyName, + member: member); /// /// Creates a named (property) argument. @@ -132,7 +194,8 @@ public static AttributeArgumentComponent CreateNamed(ICSharpSourceComponent expr type: null, expressionComponent: expressionComponent, expression: null, - name: propertyName); + name: propertyName, + member: null); /// /// Creates a named (property) argument. @@ -151,7 +214,8 @@ public static AttributeArgumentComponent CreateNamed(String expression, String p type: null, expressionComponent: null, expression: expression, - name: propertyName); + name: propertyName, + member: null); /// public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default) @@ -175,9 +239,13 @@ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellation builder.Append($"{_assignmentOperator} "); } - if (_type is { } type) + if (_member is { } member && _type is { } type) + { + builder.Append($"{type}.{member}"); + } + else if (_type is { } typeOfArgument) { - builder.Append($"typeof({type})"); + builder.Append($"typeof({typeOfArgument})"); } else if (_expressionComponent is not null) { diff --git a/Lyra/CSharpSourceBuilder.InterpolatedStringHandler.cs b/Lyra/CSharpSourceBuilder.InterpolatedStringHandler.cs index cc5d929..a20b790 100644 --- a/Lyra/CSharpSourceBuilder.InterpolatedStringHandler.cs +++ b/Lyra/CSharpSourceBuilder.InterpolatedStringHandler.cs @@ -36,13 +36,13 @@ public partial struct InterpolatedStringHandler( /// public void AppendLiteral(String literal) { - if (!writer._appendCondition) + if (!writer.IsAppendConditionMet) { return; } TryDetent(); - writer.AppendCore(literal, out var lastLineStartText); + writer.AppendCore(literal.AsSpan(), out var lastLineStartText); if (!writer._detector.TryDetectIndentation( lastLineStartText, @@ -100,7 +100,7 @@ public void AppendFormatted(ICSharpSourceComponent placeholder) /// public void AppendFormatted(T placeholder) { - if (!writer._appendCondition) + if (!writer.IsAppendConditionMet) { return; } @@ -123,7 +123,7 @@ public void AppendFormatted(T placeholder) break; case ICSharpSourceComponent component: // invert call in order to allow jit to remove boxing conversion - component.AppendTo(writer, writer._cancellationToken); + component.AppendTo(writer, writer.CancellationToken); break; case null: break; diff --git a/Lyra/CSharpSourceBuilder.cs b/Lyra/CSharpSourceBuilder.cs index 6b5a106..fe90d9b 100644 --- a/Lyra/CSharpSourceBuilder.cs +++ b/Lyra/CSharpSourceBuilder.cs @@ -4,7 +4,9 @@ namespace RhoMicro.CodeAnalysis.Lyra; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading; /// @@ -39,11 +41,12 @@ public CSharpSourceBuilder() : this(CSharpSourceBuilderOptions.Default) { } - private const Int32 AppendMethodHighOverloadPriority = 3; - private const Int32 AppendMethodMediumOverloadPriority = 2; - private const Int32 AppendMethodLowOverloadPriority = 1; - private const Int32 AppendMethodNoOverloadPriority = 0; + private const Int32 _appendMethodHighOverloadPriority = 3; + private const Int32 _appendMethodMediumOverloadPriority = 2; + private const Int32 _appendMethodLowOverloadPriority = 1; + private const Int32 _appendMethodNoOverloadPriority = 0; + private readonly Stack _appendConditions = []; private readonly List> _indentations; private readonly RentedArrayLifetime _rentedArrayLifetime; private readonly InterpolationIndentationDetectorContext _indentationDetectorContext; @@ -77,13 +80,14 @@ public CSharpSourceBuilder() : this(CSharpSourceBuilderOptions.Default) /// public Int32 Lines { get; private set; } + private Boolean IsAppendConditionMet => _appendConditions.Count == 0 || _appendConditions.Peek(); + private IInterpolationIndentationDetector _detector; private Boolean _lastWasNewLine = true; private Boolean _lastWasEmptyLine = false; private Boolean _preludeWritten = false; private Char[] _buffer; - private Boolean _appendCondition = true; - private CancellationToken _cancellationToken; + public CancellationToken CancellationToken { get; private set; } private static Int32 RoundUpToPowerOf2(Int32 value) { @@ -113,8 +117,8 @@ private Memory GetMemory(Int32 lengthHint) if (_buffer.Length < requiredLength) { var newLength = RoundUpToPowerOf2(requiredLength); - var newBuffer = Options.CharBufferOwner.Rent((Int32)newLength); - _buffer.CopyTo(newBuffer); + var newBuffer = Options.CharBufferOwner.Rent(newLength); + _buffer.CopyTo(newBuffer.AsSpan()); Options.CharBufferOwner.Return(_buffer); _buffer = newBuffer; } @@ -126,7 +130,7 @@ private Memory GetMemory(Int32 lengthHint) private void AppendCore(ReadOnlySpan text, out ReadOnlyMemory lastLineStartText) { - _cancellationToken.ThrowIfCancellationRequested(); + CancellationToken.ThrowIfCancellationRequested(); lastLineStartText = default; @@ -140,7 +144,7 @@ private void AppendCore(ReadOnlySpan text, out ReadOnlyMemory lastLi var tail = text; while (true) { - _cancellationToken.ThrowIfCancellationRequested(); + CancellationToken.ThrowIfCancellationRequested(); var nextNewline = tail.IndexOfAny('\n', '\r'); if (nextNewline is -1) @@ -195,12 +199,14 @@ private void TryWritePrelude() } _preludeWritten = true; - Options.Prelude.Invoke(this, _cancellationToken); + DetentAll(out var indentations); + Options.Prelude.Invoke(this, CancellationToken); + Indent(indentations); } private void TryWriteIndentation() { - _cancellationToken.ThrowIfCancellationRequested(); + CancellationToken.ThrowIfCancellationRequested(); if (!_lastWasNewLine) { @@ -242,7 +248,7 @@ public CSharpSourceBuilder Clear() /// public CSharpSourceBuilder SetCondition(Boolean condition) { - _appendCondition = condition; + _appendConditions.Push(condition); return this; } @@ -256,12 +262,87 @@ public CSharpSourceBuilder SetCondition(Boolean condition) /// public CSharpSourceBuilder UnsetCondition() { - _appendCondition = true; + _appendConditions.Pop(); return this; } #region Indentation + /// + /// Detents the builder fully. + /// + /// + /// The indentations removed from the builder. + /// + /// + /// A reference to the builder, for chaining of further method calls. + /// + public CSharpSourceBuilder DetentAll(out ImmutableArray> indentations) + { + if (!IsAppendConditionMet) + { + indentations = []; + return this; + } + + var result = new ReadOnlyMemory[_indentations.Count]; + for (var i = 0; i < _indentations.Count; i++) + { + result[i] = _indentations[i]; + } + + _indentations.Clear(); + + indentations = ImmutableCollectionsMarshal.AsImmutableArray(result); + return this; + } + + /// + /// Detents the builder fully. + /// + /// + /// A reference to the builder, for chaining of further method calls. + /// + public CSharpSourceBuilder DetentAll() + { + if (!IsAppendConditionMet) + { + return this; + } + + _indentations.Clear(); + + return this; + } + + /// + /// Indents the builder. + /// + /// + /// The indentations to add to the builder. + /// + /// + /// The type of enumerable containing the indentations. + /// + /// + /// A reference to the builder, for chaining of further method calls. + /// + public CSharpSourceBuilder Indent(TEnumerable indentations) + where TEnumerable : IEnumerable> + { + if (!IsAppendConditionMet) + { + return this; + } + + foreach (var indentation in indentations) + { + _indentations.Add(indentation); + } + + return this; + } + /// /// Indents the builder. /// @@ -273,7 +354,7 @@ public CSharpSourceBuilder UnsetCondition() /// public CSharpSourceBuilder Indent(ReadOnlyMemory indentation) { - if (!_appendCondition) + if (!IsAppendConditionMet) { return this; } @@ -309,7 +390,7 @@ public CSharpSourceBuilder Indent(ReadOnlyMemory indentation) /// public CSharpSourceBuilder Detent() { - if (!_appendCondition) + if (!IsAppendConditionMet) { return this; } @@ -335,17 +416,17 @@ public CSharpSourceBuilder Detent() /// /// A reference to the builder, for chaining of further method calls. /// - [OverloadResolutionPriority(AppendMethodLowOverloadPriority)] + [OverloadResolutionPriority(_appendMethodLowOverloadPriority)] public CSharpSourceBuilder Append(String text) => - Append(text.AsSpan()); + Append(text.AsSpan()); /// - [OverloadResolutionPriority(AppendMethodLowOverloadPriority)] + [OverloadResolutionPriority(_appendMethodLowOverloadPriority)] public CSharpSourceBuilder Append(ReadOnlySpan text) { - _cancellationToken.ThrowIfCancellationRequested(); + CancellationToken.ThrowIfCancellationRequested(); - if (!_appendCondition) + if (!IsAppendConditionMet) { return this; } @@ -355,19 +436,19 @@ public CSharpSourceBuilder Append(ReadOnlySpan text) } /// - [OverloadResolutionPriority(AppendMethodLowOverloadPriority)] + [OverloadResolutionPriority(_appendMethodLowOverloadPriority)] public CSharpSourceBuilder Append(Char text) => - Append([text]); + Append([text]); /// - [OverloadResolutionPriority(AppendMethodMediumOverloadPriority)] + [OverloadResolutionPriority(_appendMethodMediumOverloadPriority)] public CSharpSourceBuilder Append( - [InterpolatedStringHandlerArgument("")] - InterpolatedStringHandler text) + [InterpolatedStringHandlerArgument("")] + InterpolatedStringHandler text) { - _cancellationToken.ThrowIfCancellationRequested(); + CancellationToken.ThrowIfCancellationRequested(); - if (!_appendCondition) + if (!IsAppendConditionMet) { return this; } @@ -381,13 +462,13 @@ public CSharpSourceBuilder Append( /// /// The type of component to append, /// - [OverloadResolutionPriority(AppendMethodLowOverloadPriority)] + [OverloadResolutionPriority(_appendMethodLowOverloadPriority)] public CSharpSourceBuilder Append(T component) - where T : ICSharpSourceComponent + where T : ICSharpSourceComponent { - _cancellationToken.ThrowIfCancellationRequested(); + CancellationToken.ThrowIfCancellationRequested(); - component.AppendTo(this, _cancellationToken); + component.AppendTo(this, CancellationToken); return this; } @@ -401,12 +482,12 @@ public CSharpSourceBuilder Append(T component) /// /// A reference to the builder, for chaining of further method calls. /// - [OverloadResolutionPriority(AppendMethodNoOverloadPriority)] + [OverloadResolutionPriority(_appendMethodNoOverloadPriority)] public CSharpSourceBuilder Append(ICSharpSourceComponent component) { - _cancellationToken.ThrowIfCancellationRequested(); + CancellationToken.ThrowIfCancellationRequested(); - component.AppendTo(this, _cancellationToken); + component.AppendTo(this, CancellationToken); return this; } @@ -421,12 +502,12 @@ public CSharpSourceBuilder Append(ICSharpSourceComponent component) /// /// A reference to the builder, for chaining of further method calls. /// - [OverloadResolutionPriority(AppendMethodHighOverloadPriority)] + [OverloadResolutionPriority(_appendMethodHighOverloadPriority)] public CSharpSourceBuilder AppendLine() { - _cancellationToken.ThrowIfCancellationRequested(); + CancellationToken.ThrowIfCancellationRequested(); - if (!_appendCondition) + if (!IsAppendConditionMet) { return this; } @@ -462,10 +543,10 @@ public CSharpSourceBuilder AppendLine() /// /// A reference to the builder, for chaining of further method calls. /// - [OverloadResolutionPriority(AppendMethodLowOverloadPriority)] + [OverloadResolutionPriority(_appendMethodLowOverloadPriority)] public CSharpSourceBuilder AppendLine(String text) { - _cancellationToken.ThrowIfCancellationRequested(); + CancellationToken.ThrowIfCancellationRequested(); Append(text); AppendLine(); @@ -474,10 +555,10 @@ public CSharpSourceBuilder AppendLine(String text) } /// - [OverloadResolutionPriority(AppendMethodLowOverloadPriority)] + [OverloadResolutionPriority(_appendMethodLowOverloadPriority)] public CSharpSourceBuilder AppendLine(ReadOnlySpan text) { - _cancellationToken.ThrowIfCancellationRequested(); + CancellationToken.ThrowIfCancellationRequested(); Append(text); AppendLine(); @@ -486,10 +567,10 @@ public CSharpSourceBuilder AppendLine(ReadOnlySpan text) } /// - [OverloadResolutionPriority(AppendMethodLowOverloadPriority)] + [OverloadResolutionPriority(_appendMethodLowOverloadPriority)] public CSharpSourceBuilder AppendLine(Char text) { - _cancellationToken.ThrowIfCancellationRequested(); + CancellationToken.ThrowIfCancellationRequested(); Append(text); AppendLine(); @@ -498,14 +579,14 @@ public CSharpSourceBuilder AppendLine(Char text) } /// - [OverloadResolutionPriority(AppendMethodMediumOverloadPriority)] + [OverloadResolutionPriority(_appendMethodMediumOverloadPriority)] public CSharpSourceBuilder AppendLine( - [InterpolatedStringHandlerArgument("")] - InterpolatedStringHandler text) + [InterpolatedStringHandlerArgument("")] + InterpolatedStringHandler text) { - _cancellationToken.ThrowIfCancellationRequested(); + CancellationToken.ThrowIfCancellationRequested(); - if (!_appendCondition) + if (!IsAppendConditionMet) { return this; } @@ -521,13 +602,13 @@ public CSharpSourceBuilder AppendLine( /// /// The type of component to append, /// - [OverloadResolutionPriority(AppendMethodLowOverloadPriority)] + [OverloadResolutionPriority(_appendMethodLowOverloadPriority)] public CSharpSourceBuilder AppendLine(T component) - where T : ICSharpSourceComponent + where T : ICSharpSourceComponent { - _cancellationToken.ThrowIfCancellationRequested(); + CancellationToken.ThrowIfCancellationRequested(); - component.AppendTo(this, _cancellationToken); + component.AppendTo(this, CancellationToken); AppendLine(); return this; @@ -542,12 +623,12 @@ public CSharpSourceBuilder AppendLine(T component) /// /// A reference to the builder, for chaining of further method calls. /// - [OverloadResolutionPriority(AppendMethodNoOverloadPriority)] + [OverloadResolutionPriority(_appendMethodNoOverloadPriority)] public CSharpSourceBuilder AppendLine(ICSharpSourceComponent component) { - _cancellationToken.ThrowIfCancellationRequested(); + CancellationToken.ThrowIfCancellationRequested(); - component.AppendTo(this, _cancellationToken); + component.AppendTo(this, CancellationToken); AppendLine(); return this; @@ -557,41 +638,85 @@ public CSharpSourceBuilder AppendLine(ICSharpSourceComponent component) #region Append Type Name + /// + public CSharpSourceBuilder AppendTypeName() => AppendTypeName(Options.DefaultTypeNameOptions); + /// /// /// The type whose name to append. /// - public CSharpSourceBuilder AppendTypeName() + public CSharpSourceBuilder AppendTypeName(TypeNameOptions options) { - AppendTypeName(typeof(T)); + AppendTypeName(typeof(T), options); return this; } + /// + public CSharpSourceBuilder AppendTypeName(Type type) => AppendTypeName(type, Options.DefaultTypeNameOptions); + /// /// Appends the globally qualified C# name of a type to the builder. /// + /// + /// The options to use when appending the type name. + /// /// /// The type whose name to append. /// /// /// A reference to the builder, for chaining of further method calls. /// - public CSharpSourceBuilder AppendTypeName(Type type) + public CSharpSourceBuilder AppendTypeName(Type type, TypeNameOptions options) { - _cancellationToken.ThrowIfCancellationRequested(); + CancellationToken.ThrowIfCancellationRequested(); - if (!_appendCondition) + if (!IsAppendConditionMet) { return this; } - Append("global::"); + if (options.UseTypeAliases && _aliasedTypes.TryGetValue(type, out var aliased)) + { + Append(aliased); + } + else + { + AppendMetadataTypeName(type, options); + } - if (type.Namespace is { } ns) + return this; + } + + private static readonly Dictionary _aliasedTypes = new() + { + { typeof(Boolean), "bool" }, + { typeof(Byte), "byte" }, + { typeof(SByte), "sbyte" }, + { typeof(Int16), "short" }, + { typeof(UInt16), "ushort" }, + { typeof(Int32), "int" }, + { typeof(UInt32), "int" }, + { typeof(Int64), "long" }, + { typeof(UInt64), "long" }, + { typeof(Single), "float" }, + { typeof(Double), "double" }, + { typeof(Decimal), "decimal" }, + { typeof(Char), "char" }, + { typeof(String), "string" } + }; + + private void AppendMetadataTypeName(Type type, TypeNameOptions options) + { + if (options.UseGloballyQualifiedName) { - Append(ns); - Append('.'); + Append("global::"); + + if (type.Namespace is { } ns) + { + Append(ns); + Append('.'); + } } var firstIllegalIndex = type.Name.AsSpan().IndexOfAny('`', '['); @@ -605,25 +730,25 @@ public CSharpSourceBuilder AppendTypeName(Type type) Append("[]"); } - if (type.IsGenericType) + if (!type.IsGenericType) { - Append('<'); + return; + } - foreach (var parameter in type.GetGenericArguments()) - { - _cancellationToken.ThrowIfCancellationRequested(); + Append('<'); - AppendTypeName(parameter); - } + foreach (var parameter in type.GetGenericArguments()) + { + CancellationToken.ThrowIfCancellationRequested(); - Append('>'); + AppendTypeName(parameter, options); } - return this; + Append('>'); } #endregion - + #region Cancellation /// @@ -634,7 +759,7 @@ public CSharpSourceBuilder AppendTypeName(Type type) /// public CSharpSourceBuilder SetCancellationToken(CancellationToken cancellationToken) { - _cancellationToken = cancellationToken; + CancellationToken = cancellationToken; return this; } @@ -647,7 +772,7 @@ public CSharpSourceBuilder SetCancellationToken(CancellationToken cancellationTo /// public CSharpSourceBuilder UnsetCancellationToken() { - _cancellationToken = default; + CancellationToken = default; return this; } @@ -665,9 +790,9 @@ public CSharpSourceBuilder UnsetCancellationToken() /// A reference to the builder, for chaining of further method calls. /// public CSharpSourceBuilder SetInterpolationIndentationDetector( - IInterpolationIndentationDetector detector) + IInterpolationIndentationDetector detector) { - if (!_appendCondition) + if (!IsAppendConditionMet) { return this; } @@ -690,12 +815,12 @@ public CSharpSourceBuilder SetInterpolationIndentationDetector( /// A reference to the builder, for chaining of further method calls. /// public CSharpSourceBuilder SetInterpolationIndentationDetector( - IInterpolationIndentationDetector detector, - out IInterpolationIndentationDetector previousDetector) + IInterpolationIndentationDetector detector, + out IInterpolationIndentationDetector previousDetector) { previousDetector = _detector; - if (!_appendCondition) + if (!IsAppendConditionMet) { return this; } @@ -705,6 +830,25 @@ public CSharpSourceBuilder SetInterpolationIndentationDetector( return this; } +#endregion + +#region Miscellaneous + + /// + /// Skips checks to append the prelude until is called. + /// This will cause the prelude to be omitted if no other calls appending + /// text to the builder have been made. + /// + /// + /// A reference to the builder, for chaining of further method calls. + /// + public CSharpSourceBuilder SkipPreludeChecks() + { + _preludeWritten = true; + + return this; + } + #endregion /// diff --git a/Lyra/CSharpSourceBuilderOptions.cs b/Lyra/CSharpSourceBuilderOptions.cs index 1671355..f72c8b3 100644 --- a/Lyra/CSharpSourceBuilderOptions.cs +++ b/Lyra/CSharpSourceBuilderOptions.cs @@ -19,6 +19,11 @@ internal partial class CSharpSourceBuilderOptions /// public static CSharpSourceBuilderOptions Default { get; } = new(); + /// + /// Gets the options to use when writing type names and no other options are explicitly provided. + /// + public virtual TypeNameOptions DefaultTypeNameOptions { get; init; } = TypeNameOptions.Default; + /// /// Gets the initial indentation to apply to the builder. /// diff --git a/Lyra/ComponentFactory.Docs.cs b/Lyra/ComponentFactory.Docs.cs new file mode 100644 index 0000000..d521307 --- /dev/null +++ b/Lyra/ComponentFactory.Docs.cs @@ -0,0 +1,870 @@ +// SPDX-License-Identifier: MPL-2.0 + +namespace RhoMicro.CodeAnalysis.Lyra; + +using System.Collections.Immutable; +using Options = DocsCommentElementComponentOptions; + +#if CSHARPSOURCEBUILDER_GENERATOR +[IncludeFile] +#endif +partial class ComponentFactory +{ + /// + /// Creates docs comment component instances. + /// + public static partial class Docs + { + /// + /// Creates a new docs comment element. + /// + /// + /// The options to be used by the component. + /// + /// + /// A new docs comment element component. + /// + public static DocsCommentElementComponent Element(Options options) + => new(Attributes: null, Body: null, options); + /// + /// Creates a new docs comment element. + /// + /// + /// The state used by the component. + /// + /// + /// The options to be used by the component. + /// + /// + /// The callback to invoke when appending attributes. + /// + /// + /// The callback to invoke when appending the body. + /// + /// + /// The type of state used by the component. + /// + /// + /// A new docs comment element component. + /// + public static DocsCommentElementComponent Element( + TState state, + Options options, + Action? body, + Action? attributes) + => new(state, Attributes: attributes, Body: body, options); + + /// + /// Creates a new docs comment element. + /// + /// + /// The options to be used by the component. + /// + /// + /// The callback to invoke when appending attributes. + /// + /// + /// The callback to invoke when appending the body. + /// + /// + /// A new docs comment element component. + /// + public static DocsCommentElementComponent Element( + Options options, + Action? body, + Action? attributes) + => new(Attributes: attributes, Body: body, options); + + /// + /// Creates a new docs comment element. + /// + /// + /// The options to be used by the component. + /// + /// + /// The text to render in the element. + /// + /// + /// A new docs comment element component. + /// + public static DocsCommentElementComponent Element( + Options options, + String text) + => Element( + text, + options, + body: static (t, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + b.Append(t); + }, + attributes: null); + + /// + /// Creates a new summary element component. + /// + /// + /// The text to render in the summary element. + /// + /// + /// A new summary element component. + /// + public static DocsCommentElementComponent Summary(String text) => + Element(Options.Summary, text); + + /// + /// Creates a new summary element component. + /// + /// + /// + /// + /// + /// A new summary element component. + /// + public static DocsCommentElementComponent Summary(Action body) => + Element(Options.Summary, body: body, attributes: null); + + /// + /// Creates a new summary element component. + /// + /// + /// + /// + /// + /// + /// + /// + /// A new summary element component. + /// + public static DocsCommentElementComponent Summary( + TState state, + Action body) => + Element(state, Options.Summary, body: body, attributes: null); + + /// + /// Creates a new returns element component. + /// + /// + /// The text to render in the returns element. + /// + /// + /// A new returns element component. + /// + public static DocsCommentElementComponent Returns(String text) => + Element(Options.Returns, text); + + /// + /// Creates a new returns element component. + /// + /// + /// + /// + /// + /// A new returns element component. + /// + public static DocsCommentElementComponent Returns(Action body) => + Element(Options.Returns, body: body, attributes: null); + + /// + /// Creates a new returns element component. + /// + /// + /// + /// + /// + /// + /// + /// + /// A new returns element component. + /// + public static DocsCommentElementComponent Returns( + TState state, + Action body) => + Element(state, Options.Returns, body: body, attributes: null); + + /// + /// Creates a new remarks element component. + /// + /// + /// The text to render in the remarks element. + /// + /// + /// A new remarks element component. + /// + public static DocsCommentElementComponent Remarks(String text) => + Element(Options.Remarks, text); + + /// + /// Creates a new remarks element component. + /// + /// + /// + /// + /// + /// A new remarks element component. + /// + public static DocsCommentElementComponent Remarks(Action body) => + Element(Options.Remarks, body: body, attributes: null); + + /// + /// Creates a new remarks element component. + /// + /// + /// + /// + /// + /// + /// + /// + /// A new remarks element component. + /// + public static DocsCommentElementComponent Remarks( + TState state, + Action body) => + Element(state, Options.Remarks, body: body, attributes: null); + + /// + /// Creates a new param element component. + /// + /// + /// The text to render in the param element. + /// + /// + /// The name of the parameter documented by the param element. + /// + /// + /// A new param element component. + /// + public static DocsCommentElementComponent<(String, String)> Param(String name, String text) => + Element( + state: (name, text), + Options.Param, + attributes: static (t, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + var (name, _) = t; + + b.Append($"name=\"{name}\""); + }, + body: static (t, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + var (_, text) = t; + b.Append(text); + }); + + /// + /// Creates a new param element component. + /// + /// + /// The name of the parameter documented by the param element. + /// + /// + /// + /// + /// + /// + /// + /// + /// A new param element component. + /// + public static + DocsCommentElementComponent<(String, TState, Action)> + Param( + String name, + TState state, + Action body) => + Element( + state: (name, state, body), + Options.Param, + attributes: static (t, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + var (name, _, _) = t; + + b.Append($"name=\"{name}\""); + }, + body: static (t, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + var (_, state, body) = t; + + body.Invoke(state, b, ct); + }); + + /// + /// Creates a new param element component. + /// + /// + /// The name of the parameter documented by the param element. + /// + /// + /// + /// + /// + /// A new param element component. + /// + public static + DocsCommentElementComponent<(String, Action)> + Param(String name, Action body) => + Element( + state: (name, body), + Options.Param, + attributes: static (t, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + var (name, _) = t; + + b.Append($"name=\"{name}\""); + }, + body: static (t, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + var (_, body) = t; + + body.Invoke(b, ct); + }); + + /// + /// Creates a new typeparam element component. + /// + /// + /// The text to render in the typeparam element. + /// + /// + /// The name of the type parameter documented by the typeparam element. + /// + /// + /// A new typeparam element component. + /// + public static DocsCommentElementComponent<(String, String)> TypeParam(String name, String text) => + Element( + state: (name, text), + Options.TypeParam, + attributes: static (t, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + var (name, _) = t; + + b.Append($"name=\"{name}\""); + }, + body: static (t, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + var (_, text) = t; + b.Append(text); + }); + + /// + /// Creates a new typeparam element component. + /// + /// + /// The name of the parameter documented by the typeparam element. + /// + /// + /// + /// + /// + /// + /// + /// + /// A new typeparam element component. + /// + public static + DocsCommentElementComponent<(String, TState, Action)> + TypeParam( + String name, + TState state, + Action body) => + Element( + state: (name, state, body), + Options.TypeParam, + attributes: static (t, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + var (name, _, _) = t; + + b.Append($"name=\"{name}\""); + }, + body: static (t, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + var (_, state, body) = t; + + body.Invoke(state, b, ct); + }); + + /// + /// Creates a new see langword element component. + /// + /// + /// The name of the language word referenced by the component. + /// + /// + /// A new see element component referencing the language word. + /// + public static DocsCommentElementComponent Langword(String word) => + Element( + state: word, + Options.See, + body: null, + attributes: static (w, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + b.Append($"langword=\"{w}\""); + }); + + /// + /// Creates a new see cref element component. + /// + /// + /// The member referenced by the see element. + /// + /// + /// A new see element component referencing the member. + /// + public static DocsCommentElementComponent Cref(String memberReference) => + Element( + state: memberReference, + Options.See, + body: null, + attributes: static (m, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + b.Append($"cref=\"{m}\""); + }); + + /// + /// Creates a new see cref element component. + /// + /// + /// The state used by . + /// + /// + /// The callback invoked when appending the member referenced by the see element. + /// + /// + /// The type of state used by . + /// + /// + /// A new see element component referencing the member. + /// + public static DocsCommentElementComponent<(TState, Action)> + Cref( + TState state, + Action memberReference) => + Element( + state: (state, memberReference), + Options.See, + body: null, + attributes: static (t, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + var (state, memberReference) = t; + + b.Append("cref=\""); + memberReference.Invoke(state, b, ct); + b.Append("\""); + }); + + /// + /// Creates a new paramref element component. + /// + /// + /// The name of the parameter referenced by the paramref element. + /// + /// + /// A new paramref element component. + /// + public static DocsCommentElementComponent ParamRef(String name) => + Element( + state: name, + Options.ParamRef, + body: null, + attributes: static (n, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + b.Append($"name=\"{n}\""); + }); + + /// + /// Creates a new typeparamref element component. + /// + /// + /// The name of the type parameter referenced by the typeparamref element. + /// + /// + /// A new typeparamref element component. + /// + public static DocsCommentElementComponent TypeParamRef(String name) => + Element( + state: name, + Options.TypeParamRef, + body: null, + attributes: static (n, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + b.Append($"name=\"{n}\""); + }); + + /// + /// Creates a new inheritdoc element component. + /// + /// + /// The member referenced by the inheritdoc element. + /// + /// + /// A new inheritdoc element component. + /// + public static DocsCommentElementComponent Inheritdoc(String? reference = null) => + Element( + state: reference, + Options.Inheritdoc, + body: null, + attributes: static (r, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + if (r is null) + { + return; + } + + b.Append($"cref=\"{r}\""); + }); + + /// + /// Creates a new c element component. + /// + /// + /// The text to render in the c element. + /// + /// + /// A new c element component. + /// + public static DocsCommentElementComponent C(String text) => + Element(Options.C, text); + + /// + /// Creates a new c element component. + /// + /// + /// + /// + /// + /// A new c element component. + /// + public static DocsCommentElementComponent C(Action body) => + Element(Options.C, body: body, attributes: null); + + /// + /// Creates a new c element component. + /// + /// + /// + /// + /// + /// + /// + /// + /// A new c element component. + /// + public static DocsCommentElementComponent C( + TState state, + Action body) => + Element(state, Options.C, body: body, attributes: null); + + + /// + /// Creates a new term element component. + /// + /// + /// The text to render in the term element. + /// + /// + /// A new term element component. + /// + public static DocsCommentElementComponent Term(String text) => + Element(Options.Term, text); + + /// + /// Creates a new term element component. + /// + /// + /// + /// + /// + /// A new term element component. + /// + public static DocsCommentElementComponent Term(Action body) => + Element(Options.Term, body: body, attributes: null); + + /// + /// Creates a new term element component. + /// + /// + /// + /// + /// + /// + /// + /// + /// A new term element component. + /// + public static DocsCommentElementComponent Term( + TState state, + Action body) => + Element(state, Options.Term, body: body, attributes: null); + + + /// + /// Creates a new description element component. + /// + /// + /// The text to render in the description element. + /// + /// + /// A new description element component. + /// + public static DocsCommentElementComponent Description(String text) => + Element(Options.Description, text); + + /// + /// Creates a new description element component. + /// + /// + /// + /// + /// + /// A new description element component. + /// + public static DocsCommentElementComponent Description(Action body) => + Element(Options.Description, body: body, attributes: null); + + /// + /// Creates a new description element component. + /// + /// + /// + /// + /// + /// + /// + /// + /// A new description element component. + /// + public static DocsCommentElementComponent Description( + TState state, + Action body) => + Element(state, Options.Description, body: body, attributes: null); + + /// + /// Creates a new list element component. + /// + /// + /// The type of the list component. + /// + /// + /// + /// + /// + /// + /// + /// + /// A new list element component. + /// + public static DocsCommentElementComponent< + (String type, + TState state, + Action body)> + List( + String type, + TState state, + Action body) => + Element( + (type, state, body), + Options.List, + body: static (t, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + var (_, state, body) = t; + + body.Invoke(state, b, ct); + }, + attributes: static (t, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + var (type, _, _) = t; + + b.Append($"type=\"{type}\""); + }); + + /// + /// Creates a new item element component. + /// + /// + /// + /// + /// + /// The callback invoked when appending the term child of the item element. + /// + /// + /// The callback invoked when appending the description child of the item element. + /// + /// + /// A new item element component. + /// + public static DocsCommentElementComponent< + (TState state, + Action? term, + Action? description)> + Item( + TState state, + Action? term, + Action? description) => + Element( + (state, term, description), + Options.Item, + body: static (t, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + var (state, term, description) = t; + + if (term is not null) + { + b.Append(Term(state, term)); + } + + if (description is null) + { + return; + } + + if (term is not null) + { + b.AppendLine(); + } + + b.Append(Description(state, description)); + }, attributes: null); + + /// + /// Creates a new listheader element component. + /// + /// + /// + /// + /// + /// The callback invoked when appending the term child of the listheader element. + /// + /// + /// The callback invoked when appending the description child of the listheader element. + /// + /// + /// A new listheader element component. + /// + public static DocsCommentElementComponent< + (TState state, + Action? term, + Action? description)> + ListHeader( + TState state, + Action? term, + Action? description) => + Element( + (state, term, description), + Options.ListHeader, + body: static (t, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + var (state, term, description) = t; + + if (term is not null) + { + b.Append(Term(state, term)); + } + + if (description is null) + { + return; + } + + if (term is not null) + { + b.AppendLine(); + } + + b.Append(Description(state, description)); + }, attributes: null); + + /// + /// Creates a new listheader element component. + /// + /// + /// The text to append as the body of the term child of the listheader element. + /// + /// + /// The text to append as the body of the description child of the listheader element. + /// + /// + /// A new listheader element component. + /// + public static DocsCommentElementComponent<(String? term, String? description)> ListHeader( + String? term, + String? description) => + Element( + (term, description), + Options.ListHeader, + body: static (t, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + var (term, description) = t; + + if (term is not null) + { + b.Append(Term(term)); + } + + if (description is null) + { + return; + } + + if (term is not null) + { + b.AppendLine(); + } + + b.Append(Description(description)); + }, attributes: null); + + /// + /// Creates a new br element. + /// + /// + /// A new br element. + /// + public static DocsCommentElementComponent Br() => Element(Options.Br); + } +} diff --git a/Lyra/ComponentFactory.cs b/Lyra/ComponentFactory.cs index 92786ab..d2242fc 100644 --- a/Lyra/ComponentFactory.cs +++ b/Lyra/ComponentFactory.cs @@ -4,6 +4,7 @@ namespace RhoMicro.CodeAnalysis.Lyra; using System; using System.Collections.Immutable; +using System.Net.Sockets; using System.Threading; /// @@ -17,8 +18,8 @@ internal static partial class ComponentFactory /// /// Creates a new list component. /// - /// - /// The elements to enumerate. + /// + /// The list to render. /// /// /// The separator separating each element. @@ -29,29 +30,30 @@ internal static partial class ComponentFactory /// /// A new list component. /// - public static ListComponent List( - ImmutableArray elements, - String separator = "", - String terminator = "") - => new(Elements: elements, - Append: static (e, _, _, b, ct) => - { - ct.ThrowIfCancellationRequested(); - b.Append(e); - }, - Separator: separator, - Separate: static (s, _, _, b, ct) => - { - ct.ThrowIfCancellationRequested(); - b.Append(s); - }, - Terminator: terminator); + public static ListComponent List( + TList list, + String separator = "", + String terminator = "") + where TList : IList + => new(list, + Append: static (e, _, _, b, ct) => + { + ct.ThrowIfCancellationRequested(); + b.Append(e); + }, + Separator: separator, + Separate: static (s, _, _, b, ct) => + { + ct.ThrowIfCancellationRequested(); + b.Append(s); + }, + Terminator: terminator); /// /// Creates a new list component. /// - /// - /// The elements to enumerate. + /// + /// The list to render. /// /// /// The callback to invoke to append an element. @@ -65,20 +67,21 @@ public static ListComponent List( /// /// A new list component. /// - public static ListComponent List( - ImmutableArray elements, - Action append, - String separator = "", - String terminator = "") - => new(Elements: elements, - Append: append, - Separator: separator, - Separate: static (s, _, _, b, ct) => - { - ct.ThrowIfCancellationRequested(); - b.Append(s); - }, - Terminator: terminator); + public static ListComponent List( + TList list, + Action append, + String separator = "", + String terminator = "") + where TList : IList + => new(List: list, + Append: append, + Separator: separator, + Separate: static (s, _, _, b, ct) => + { + ct.ThrowIfCancellationRequested(); + b.Append(s); + }, + Terminator: terminator); /// /// Creates a new namespace component. @@ -96,9 +99,23 @@ public static ListComponent List( /// A new namespace component. /// public static NamespaceComponent Namespace(String @namespace, TBody body) - where TBody : ICSharpSourceComponent + where TBody : ICSharpSourceComponent => new(@namespace, body); + /// + /// Creates a new type name component. + /// + /// + /// The options to use when appending the type name. + /// + /// + /// The type whose name to append. + /// + /// + /// A new type name component. + /// + public static TypeNameComponent TypeName(Type type, TypeNameOptions options) => new(type, options); + /// /// Creates a new type name component. /// @@ -110,6 +127,123 @@ public static NamespaceComponent Namespace(String @namespace, TBod /// public static TypeNameComponent TypeName(Type type) => new(type); + /// + /// Creates a new type name component. + /// + /// + /// The options to use when appending the type name. + /// + /// + /// The type whose name to append. + /// + /// + /// A new type name component. + /// + public static TypeNameComponent TypeName(TypeNameOptions options) => new(typeof(T), options); + + /// + /// Creates a new type name component. + /// + /// + /// The type whose name to append. + /// + /// + /// A new type name component. + /// + public static TypeNameComponent TypeName() => new(typeof(T)); + + /// + /// Gets a backing type for enums with no more than members. + /// + /// + /// The options to use when appending the type name. + /// + /// + /// The amount of members in the enum. + /// + /// + /// A type name for the smallest required backing type to accommodate enum members. + /// + public static TypeNameComponent EnumBackingType(Int32 memberCount, TypeNameOptions options) + => memberCount switch + { + <= Byte.MaxValue + 1 => TypeName(options), + <= Int16.MaxValue + 1 => TypeName(options), + <= Int32.MaxValue => TypeName(options), + }; + + /// + /// Gets a backing type for enums with no more than members. + /// + /// + /// The amount of members in the enum. + /// + /// + /// A type name for the smallest required backing type to accommodate enum members. + /// + public static TypeNameComponent EnumBackingType(Int32 memberCount) + => memberCount switch + { + <= Byte.MaxValue + 1 => TypeName(), + <= Int16.MaxValue + 1 => TypeName(), + <= Int32.MaxValue => TypeName(), + }; + + /// + /// Gets a backing type for flags enums with no more than flags. + /// + /// + /// For example, to represent 16 flags + the None = 0 member, 27 should be passed: + /// + /// var backingType = FlagsEnumBackingType(16); // short: 16 flags + None + /// + /// + /// + /// The options to use when appending the type name. + /// + /// + /// The amount of flags in the enum. + /// + /// + /// A type name for the smallest required backing type to accommodate individual flags, + /// + public static TypeNameComponent FlagsEnumBackingType(Int32 flagsCount, TypeNameOptions options) + => flagsCount switch + { + <= 8 => TypeName(options), + <= 16 => TypeName(options), + <= 32 => TypeName(options), + <= 64 => TypeName(options), + _ => throw new ArgumentOutOfRangeException(nameof(flagsCount), flagsCount, + $"{nameof(flagsCount)} must be less than 65.") + }; + + /// + /// Gets a backing type for flags enums with no more than flags. + /// + /// + /// For example, to represent 16 flags + the None = 0 member, 27 should be passed: + /// + /// var backingType = FlagsEnumBackingType(16); // short: 16 flags + None + /// + /// + /// + /// The amount of flags in the enum. + /// + /// + /// A type name for the smallest required backing type to accommodate individual flags, + /// + public static TypeNameComponent FlagsEnumBackingType(Int32 flagsCount) + => flagsCount switch + { + <= 8 => TypeName(), + <= 16 => TypeName(), + <= 32 => TypeName(), + <= 64 => TypeName(), + _ => throw new ArgumentOutOfRangeException(nameof(flagsCount), flagsCount, + $"{nameof(flagsCount)} must be less than 65.") + }; + /// /// Creates a new type name component. /// @@ -134,8 +268,8 @@ public static NamespaceComponent Namespace(String @namespace, TBod /// A new attribute argument component. /// public static AttributeArgumentComponent PositionalAttributeArgument( - TypeNameComponent type, - String? parameterName = null) + TypeNameComponent type, + String? parameterName = null) => AttributeArgumentComponent.CreatePositional(type, parameterName); /// @@ -151,8 +285,8 @@ public static AttributeArgumentComponent PositionalAttributeArgument( /// A new attribute argument component. /// public static AttributeArgumentComponent PositionalAttributeArgument( - ICSharpSourceComponent expressionComponent, - String? parameterName = null) + ICSharpSourceComponent expressionComponent, + String? parameterName = null) => AttributeArgumentComponent.CreatePositional(expressionComponent, parameterName); /// @@ -168,8 +302,8 @@ public static AttributeArgumentComponent PositionalAttributeArgument( /// A new attribute argument component. /// public static AttributeArgumentComponent PositionalAttributeArgument( - String expression, - String? parameterName = null) + String expression, + String? parameterName = null) => AttributeArgumentComponent.CreatePositional(expression, parameterName); /// @@ -185,8 +319,8 @@ public static AttributeArgumentComponent PositionalAttributeArgument( /// A new attribute argument component. /// public static AttributeArgumentComponent NamedAttributeArgument( - TypeNameComponent type, - String propertyName) + TypeNameComponent type, + String propertyName) => AttributeArgumentComponent.CreateNamed(type, propertyName); /// @@ -202,8 +336,8 @@ public static AttributeArgumentComponent NamedAttributeArgument( /// A new attribute argument component. /// public static AttributeArgumentComponent NamedAttributeArgument( - ICSharpSourceComponent expressionComponent, - String propertyName) + ICSharpSourceComponent expressionComponent, + String propertyName) => AttributeArgumentComponent.CreateNamed(expressionComponent, propertyName); /// @@ -219,8 +353,8 @@ public static AttributeArgumentComponent NamedAttributeArgument( /// A new attribute argument component. /// public static AttributeArgumentComponent NamedAttributeArgument( - String expression, - String propertyName) + String expression, + String propertyName) => AttributeArgumentComponent.CreateNamed(expression, propertyName); /// @@ -236,8 +370,8 @@ public static AttributeArgumentComponent NamedAttributeArgument( /// A new attribute component. /// public static AttributeComponent Attribute( - TypeNameComponent type, - ImmutableArray arguments) + TypeNameComponent type, + params ImmutableArray arguments) => new(type, arguments); /// @@ -268,28 +402,28 @@ public static AttributeComponent Attribute( /// A new type component. /// public static TypeComponent Type( - String modifiers, - String name, - TBody body, - ImmutableArray typeParameters = default, - ImmutableArray baseTypeList = default, - ImmutableArray attributes = default) - where TBody : ICSharpSourceComponent + String modifiers, + String name, + TBody body, + ImmutableArray typeParameters = default, + ImmutableArray baseTypeList = default, + ImmutableArray attributes = default) + where TBody : ICSharpSourceComponent => new( - Modifiers: modifiers, - Name: name, - TypeParameters: typeParameters.IsDefault ? [] : typeParameters, - BaseTypeList: baseTypeList.IsDefault ? [] : baseTypeList, - Attributes: attributes.IsDefault ? [] : attributes, - Body: body); + Modifiers: modifiers, + Name: name, + TypeParameters: typeParameters.IsDefault ? [] : typeParameters, + BaseTypeList: baseTypeList.IsDefault ? [] : baseTypeList, + Attributes: attributes.IsDefault ? [] : attributes, + Body: body); /// public static TypeComponent Type( - String modifiers, - String name, - ImmutableArray typeParameters = default, - ImmutableArray baseTypeList = default, - ImmutableArray attributes = default) + String modifiers, + String name, + ImmutableArray typeParameters = default, + ImmutableArray baseTypeList = default, + ImmutableArray attributes = default) => Type(modifiers, name, EmptyComponent.Instance, typeParameters, baseTypeList, attributes); /// @@ -302,4 +436,42 @@ public static TypeComponent Type( /// A new strategy component. /// public static StrategyComponent Create(Action append) => new(append); + + /// + /// Creates a new strategy based component. + /// + /// + /// The strategy to use when appending. + /// + /// + /// The state used by the component. + /// + /// + /// The type of state used by the component. + /// + /// + /// A new strategy component. + /// + public static StrategyComponent Create( + TState state, + Action append) => new(state, append); + + /// + /// Creates a new region component. + /// + /// + /// The name of the region. + /// + /// + /// The body of the region. + /// + /// + /// The type of the body. + /// + /// + /// A new region component. + /// + public static RegionComponent Region(String name, TBody body) + where TBody : ICSharpSourceComponent + => new(name, body); } diff --git a/Lyra/DocsCommentElementComponent.cs b/Lyra/DocsCommentElementComponent.cs new file mode 100644 index 0000000..07a81d0 --- /dev/null +++ b/Lyra/DocsCommentElementComponent.cs @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: MPL-2.0 + +namespace RhoMicro.CodeAnalysis.Lyra; + +/// +/// Implements a docs comment XML element. +/// +/// +/// This type does not support value equality. +/// +/// +/// The state used by the component. +/// +/// +/// The callback to invoke when appending attributes. +/// +/// +/// The callback to invoke when appending the body. +/// +/// +/// The options used by the component. +/// +/// +/// The type of state used by the component. +/// +#if CSHARPSOURCEBUILDER_GENERATOR +[IncludeFile] +#endif +internal readonly record struct DocsCommentElementComponent( + TState State, + Action? Attributes, + Action? Body, + DocsCommentElementComponentOptions Options) + : ICSharpSourceComponent +{ + /// + public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (Options.IndentSelfWithComment) + { + builder.Indent("/// "); + } + + builder.Append('<'); + + builder.Append(Options.Element); + + if (Attributes is not null) + { + builder.Append(' '); + Attributes.Invoke(State, builder, cancellationToken); + } + + if (Body is not null) + { + builder.Append('>'); + + if (Options.Multiline) + { + builder.AppendLine(); + } + + if (Options.IndentBody) + { + builder.Indent(); + } + + Body.Invoke(State, builder, cancellationToken); + + if (Options.IndentBody) + { + builder.Detent(); + } + + if (Options.Multiline) + { + builder.AppendLine(); + } + + builder.Append($"'); + + if (Options.IndentSelfWithComment) + { + builder.Detent(); + } + } + + /// + public Boolean Equals(DocsCommentElementComponent 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."); +} + +/// +/// Implements a docs comment XML element. +/// +/// +/// The callback to invoke when appending attributes. +/// +/// +/// The callback to invoke when appending the body. +/// +/// +/// The options used by the component. +/// +#if CSHARPSOURCEBUILDER_GENERATOR +[IncludeFile] +#endif +internal readonly record struct DocsCommentElementComponent( + Action? Attributes, + Action? Body, + DocsCommentElementComponentOptions Options) + : ICSharpSourceComponent +{ + /// + public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (Options.IndentSelfWithComment) + { + builder.Indent("/// "); + } + + builder.Append('<'); + + builder.Append(Options.Element); + + if (Attributes is not null) + { + builder.Append(' '); + Attributes.Invoke(builder, cancellationToken); + } + + if (Body is not null) + { + builder.Append('>'); + + if (Options.Multiline) + { + builder.AppendLine(); + } + + if (Options.IndentBody) + { + builder.Indent(); + } + + Body.Invoke(builder, cancellationToken); + + if (Options.IndentBody) + { + builder.Detent(); + } + + if (Options.Multiline) + { + builder.AppendLine(); + } + + builder.Append($"'); + + if (Options.IndentSelfWithComment) + { + builder.Detent(); + } + } + + /// + public Boolean Equals(DocsCommentElementComponent 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/Lyra/DocsCommentElementComponentOptions.cs b/Lyra/DocsCommentElementComponentOptions.cs new file mode 100644 index 0000000..cdd701f --- /dev/null +++ b/Lyra/DocsCommentElementComponentOptions.cs @@ -0,0 +1,163 @@ +// SPDX-License-Identifier: MPL-2.0 + +namespace RhoMicro.CodeAnalysis.Lyra; + +using System.Runtime.CompilerServices; + +/// +/// Provides options for rendering docs comment elements. +/// +/// +/// The name of the element. +/// +/// +/// Indicates whether to indent using /// regardless of whether +/// is or not. +/// +/// +/// Indicates whether to render docs comment elements on multiple lines. +/// This should be set to for elements whose contents +/// span multiple lines. +/// +/// +/// Indicates whether to indent the body using the builders default indentation. +/// +#if CSHARPSOURCEBUILDER_GENERATOR +[IncludeFile] +#endif +internal readonly record struct DocsCommentElementComponentOptions( + String Element, + Boolean IndentSelfWithComment, + Boolean Multiline, + Boolean IndentBody) +{ + private static DocsCommentElementComponentOptions CreateRoot([CallerMemberName] String element = "") => + new(Element: element.ToLowerInvariant(), + IndentSelfWithComment: true, + Multiline: true, + IndentBody: false); + + private static DocsCommentElementComponentOptions CreateMultilineChild([CallerMemberName] String element = "") => + new(Element: element.ToLowerInvariant(), + IndentSelfWithComment: false, + Multiline: true, + IndentBody: false); + + private static DocsCommentElementComponentOptions CreateInlineChild([CallerMemberName] String element = "") => + new(Element: element.ToLowerInvariant(), + IndentSelfWithComment: false, + Multiline: false, + IndentBody: false); + + private static DocsCommentElementComponentOptions + CreateIndentingMultilineChild([CallerMemberName] String element = "") => + new(Element: element.ToLowerInvariant(), + IndentSelfWithComment: false, + Multiline: true, + IndentBody: true); + + /// + /// Gets options for the summary element. + /// + public static DocsCommentElementComponentOptions Summary { get; } = CreateRoot(); + + /// + /// Gets options for the remarks element. + /// + public static DocsCommentElementComponentOptions Remarks { get; } = CreateRoot(); + + /// + /// Gets options for the returns element. + /// + public static DocsCommentElementComponentOptions Returns { get; } = CreateRoot(); + + /// + /// Gets options for the param element. + /// + public static DocsCommentElementComponentOptions Param { get; } = CreateRoot(); + + /// + /// Gets options for the typeparam element. + /// + public static DocsCommentElementComponentOptions TypeParam { get; } = CreateRoot(); + + /// + /// Gets options for the inheritdoc element. + /// + public static DocsCommentElementComponentOptions Inheritdoc { get; } = CreateRoot(); + + /// + /// Gets options for the c element. + /// + public static DocsCommentElementComponentOptions C { get; } = CreateInlineChild(); + + /// + /// Gets options for the b element. + /// + public static DocsCommentElementComponentOptions B { get; } = CreateInlineChild(); + + /// + /// Gets options for the br element. + /// + public static DocsCommentElementComponentOptions Br { get; } = CreateInlineChild(); + + /// + /// Gets options for the i element. + /// + public static DocsCommentElementComponentOptions I { get; } = CreateInlineChild(); + + /// + /// Gets options for the em element. + /// + public static DocsCommentElementComponentOptions Em { get; } = CreateInlineChild(); + + /// + /// Gets options for the paramref element. + /// + public static DocsCommentElementComponentOptions ParamRef { get; } = CreateInlineChild(); + + /// + /// Gets options for the typeparamref element. + /// + public static DocsCommentElementComponentOptions TypeParamRef { get; } = CreateInlineChild(); + + /// + /// Gets options for the see element. + /// + public static DocsCommentElementComponentOptions See { get; } = CreateInlineChild(); + + /// + /// Gets options for the code element. + /// + public static DocsCommentElementComponentOptions Code { get; } = CreateMultilineChild(); + + /// + /// Gets options for the br element. + /// + public static DocsCommentElementComponentOptions Para { get; } = CreateMultilineChild(); + + /// + /// Gets options for the list element. + /// + public static DocsCommentElementComponentOptions List { get; } = CreateIndentingMultilineChild(); + + /// + /// Gets options for the term element. + /// + public static DocsCommentElementComponentOptions Term { get; } = CreateIndentingMultilineChild(); + + /// + /// Gets options for the description element. + /// + public static DocsCommentElementComponentOptions Description { get; } = CreateIndentingMultilineChild(); + + /// + /// Gets options for the listheader element. + /// + public static DocsCommentElementComponentOptions ListHeader { get; } = CreateIndentingMultilineChild(); + + /// + /// Gets options for the item element. + /// + public static DocsCommentElementComponentOptions Item { get; } = CreateIndentingMultilineChild(); +} diff --git a/Lyra/Generator.cs b/Lyra/Generator.cs index 2f8ade2..3a1cacb 100644 --- a/Lyra/Generator.cs +++ b/Lyra/Generator.cs @@ -9,7 +9,7 @@ namespace RhoMicro.CodeAnalysis.Lyra; /// Generates sources for using the and related types. /// [Generator(LanguageNames.CSharp)] -public class Generator : IIncrementalGenerator +public sealed class Generator : IIncrementalGenerator { /// public void Initialize(IncrementalGeneratorInitializationContext context) diff --git a/Lyra/ListComponent.cs b/Lyra/ListComponent.cs index 89f7c98..a202fca 100644 --- a/Lyra/ListComponent.cs +++ b/Lyra/ListComponent.cs @@ -14,7 +14,7 @@ namespace RhoMicro.CodeAnalysis.Lyra; /// This type supports value equality. The and /// members are not taken into account when calculating equality. /// -/// +/// /// The elements to append to the builder. /// /// @@ -29,6 +29,9 @@ namespace RhoMicro.CodeAnalysis.Lyra; /// /// The terminator to append to the final element. /// +/// +/// The type of list to render. +/// /// /// The type of element to append. /// @@ -38,25 +41,21 @@ namespace RhoMicro.CodeAnalysis.Lyra; #if CSHARPSOURCEBUILDER_GENERATOR [IncludeFile] #endif -internal readonly record struct ListComponent( - ImmutableArray Elements, - Action Append, - TSeparator Separator, - Action Separate, - TSeparator Terminator) - : ICSharpSourceComponent +internal readonly record struct ListComponent( + TList List, + Action Append, + TSeparator Separator, + Action Separate, + TSeparator Terminator) + : ICSharpSourceComponent + where TList : IList { /// public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - if (Elements.IsDefault) - { - return; - } - - var length = Elements.Length; + var length = List.Count; for (var index = 0; index < length; index++) { @@ -67,7 +66,7 @@ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellation Separate.Invoke(Separator, index, length, builder, cancellationToken); } - var element = Elements[index]; + var element = List[index]; Append.Invoke(element, index, length, builder, cancellationToken); if (index == length - 1) @@ -78,7 +77,7 @@ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellation } /// - public Boolean Equals(ListComponent other) + public Boolean Equals(ListComponent other) { if (!EqualityComparer.Default.Equals(other.Separator, Separator)) { @@ -90,7 +89,7 @@ public Boolean Equals(ListComponent other) return false; } - if (!ImmutableArrayEqualityComparer.Equals(other.Elements, Elements)) + if (!other.List.SequenceEqual(List)) { return false; } @@ -104,7 +103,12 @@ public override Int32 GetHashCode() var hc = new HashCode(); hc.Add(Separator); hc.Add(Terminator); - hc.Add(Elements, ImmutableArrayEqualityComparer.Default); + + foreach (var element in List) + { + hc.Add(element); + } + var result = hc.ToHashCode(); return result; diff --git a/Lyra/Lyra.csproj b/Lyra/Lyra.csproj index 85e029b..ee93ddc 100644 --- a/Lyra/Lyra.csproj +++ b/Lyra/Lyra.csproj @@ -6,7 +6,8 @@ true true true - + 1.2.0 + @@ -30,9 +31,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Lyra/NamespaceComponent.cs b/Lyra/NamespaceComponent.cs index 1fce513..f4d75cc 100644 --- a/Lyra/NamespaceComponent.cs +++ b/Lyra/NamespaceComponent.cs @@ -42,11 +42,11 @@ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellation builder.AppendLine($"namespace {Name}").AppendLine("{").Indent(); } - builder.Append(Body); + builder.AppendLine(Body); if (Name is not []) { - builder.Detent().AppendLine("}"); + builder.Detent().Append("}"); } } } diff --git a/Lyra/README.md b/Lyra/README.md index e02068d..03a9e44 100644 --- a/Lyra/README.md +++ b/Lyra/README.md @@ -17,16 +17,16 @@ This project is licensed under the `MPL-2.0` license. .NET CLI: ``` -dotnet add package RhoMicro.CodeAnalysis.Lyra --version 1.0.0 +dotnet add package RhoMicro.CodeAnalysis.Lyra --version 1.1.0 ``` PackageReference: ```xml - - - - + + all + runtime; build; native; contentfiles; analyzers + ``` ## How To Use @@ -141,3 +141,44 @@ Note how the initial indentation in front of `foo` is carried along. The detection of these indentations can be configured using the `CSharpSourceBuilderOptions.InitialInterpolationIndentationDetector` property. + +### Components + +Some useful components are provided out of the box: +```cs +var body = ComponentFactory.Create( + myModel, + static (myModel, b, ct) => + { + ct.ThrowIfCancellationRequested(); + + b.Append(myModel.Value); + }); + +var type = ComponentFactory.Type( + "partial struct", + myModel.Name, + body, + baseTypeList: + [ + TypeName() + ]); + +var @namespace = ComponentFactory.Namespace( + myModel.Namespace, + type); + +builder.AppendLine(@namespace); +``` + +The output will look similar to this: +```cs +namespace Namespace +{ + partial struct Name : global::System.IComparable + { + // myModel.Value + } +} + +``` diff --git a/Lyra/RegionComponent.cs b/Lyra/RegionComponent.cs new file mode 100644 index 0000000..95c5932 --- /dev/null +++ b/Lyra/RegionComponent.cs @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MPL-2.0 + +namespace RhoMicro.CodeAnalysis.Lyra; + +/// +/// Represents a #region. +/// +/// +/// This type supports value equality. +/// +/// +/// The name of the region. +/// +/// +/// The body of the region. +/// +/// +/// The type of the body. +/// +#if CSHARPSOURCEBUILDER_GENERATOR +[IncludeFile] +#endif +internal readonly record struct RegionComponent(String Name, TBody Body) : ICSharpSourceComponent + where TBody : ICSharpSourceComponent +{ + /// + public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + builder.DetentAll(out var indentations) + .Indent(builder.Options.InitialIndentation) + .AppendLine($"#region {Name}") + .Indent(indentations.Skip(builder.Options.InitialIndentation.Length)) + .AppendLine() + .AppendLine(Body) + .AppendLine() + .DetentAll(out indentations) + .Indent(builder.Options.InitialIndentation) + .Append("#endregion") + .Indent(indentations.Skip(builder.Options.InitialIndentation.Length)); + } + + /// + public Boolean Equals(RegionComponent other) + { + if (other.Name != Name) + { + return false; + } + + if (!EqualityComparer.Default.Equals(other.Body, Body)) + { + return false; + } + + return true; + } + + /// + public override Int32 GetHashCode() + { + var hc = new HashCode(); + hc.Add(Name); + hc.Add(Body); + var result = hc.ToHashCode(); + + return result; + } +} diff --git a/Lyra/StrategyComponent.cs b/Lyra/StrategyComponent.cs index fbe9250..cc423f3 100644 --- a/Lyra/StrategyComponent.cs +++ b/Lyra/StrategyComponent.cs @@ -8,27 +8,66 @@ namespace RhoMicro.CodeAnalysis.Lyra; /// /// This type does not support value equality. /// -/// +/// /// The callback to invoke when appending to a builder. /// #if CSHARPSOURCEBUILDER_GENERATOR [IncludeFile] #endif -readonly record struct StrategyComponent(Action append) : ICSharpSourceComponent +readonly record struct StrategyComponent(Action Append) : ICSharpSourceComponent { /// public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - append.Invoke(builder, cancellationToken); + Append.Invoke(builder, cancellationToken); } /// public Boolean Equals(StrategyComponent other) => - throw new NotSupportedException("Equals is not supported on this type."); + throw new NotSupportedException("Equals is not supported on this type."); /// public override Int32 GetHashCode() => - throw new NotSupportedException("GetHashCode is not supported on this type."); + throw new NotSupportedException("GetHashCode is not supported on this type."); +} + +/// +/// Implements an arbitrary component that uses state. +/// +/// +/// This type does not support value equality. +/// +/// +/// The state used by the component. +/// +/// +/// The callback to invoke when appending to a builder. +/// +/// +/// The type of state used by the component. +/// +#if CSHARPSOURCEBUILDER_GENERATOR +[IncludeFile] +#endif +readonly record struct StrategyComponent( + TState State, + Action Append) : ICSharpSourceComponent +{ + /// + public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + Append.Invoke(State, builder, cancellationToken); + } + + /// + public Boolean Equals(StrategyComponent 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/Lyra/TypeComponent.cs b/Lyra/TypeComponent.cs index 6f8cdb0..9b80f5e 100644 --- a/Lyra/TypeComponent.cs +++ b/Lyra/TypeComponent.cs @@ -112,9 +112,9 @@ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellation builder.AppendLine() .AppendLine('{') .Indent() - .Append(Body) + .AppendLine(Body) .Detent() - .AppendLine('}'); + .Append('}'); } /// diff --git a/Lyra/TypeNameComponent.cs b/Lyra/TypeNameComponent.cs index 63034ae..599c435 100644 --- a/Lyra/TypeNameComponent.cs +++ b/Lyra/TypeNameComponent.cs @@ -22,9 +22,14 @@ namespace RhoMicro.CodeAnalysis.Lyra; /// /// The type whose name to append. /// - public TypeNameComponent(Type type) + /// + /// The options to use when appending the type name, or + /// to use the builders default type name options. + /// + public TypeNameComponent(Type type, TypeNameOptions? options = null) { _type = type; + _options = options; _typeName = null; } @@ -40,6 +45,7 @@ public TypeNameComponent(String typeName) _typeName = typeName; } + private readonly TypeNameOptions? _options; private readonly Type? _type; private readonly String? _typeName; @@ -50,7 +56,14 @@ public void AppendTo(CSharpSourceBuilder builder, CancellationToken cancellation if (_type is not null) { - builder.AppendTypeName(_type); + if (_options is { } o) + { + builder.AppendTypeName(_type, o); + } + else + { + builder.AppendTypeName(_type); + } } else if (_typeName is not null) { diff --git a/Lyra/TypeNameOptions.cs b/Lyra/TypeNameOptions.cs new file mode 100644 index 0000000..e958998 --- /dev/null +++ b/Lyra/TypeNameOptions.cs @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MPL-2.0 + +namespace RhoMicro.CodeAnalysis.Lyra; + +/// +/// Provides options for appending type names. +/// +/// +/// Indicates whether primitives should be appended using their language +/// alias, as opposed to their fully qualified framework name. +/// +/// +/// Indicates whether type names should be globally qualified. +/// +#if CSHARPSOURCEBUILDER_GENERATOR +[IncludeFile] +#endif +internal readonly record struct TypeNameOptions( + Boolean UseTypeAliases = true, + Boolean UseGloballyQualifiedName = true) +{ + /// + /// Gets the default instance. + /// + public static TypeNameOptions Default { get; } = new(UseTypeAliases: true, UseGloballyQualifiedName: true); +} diff --git a/Polyfills/Generator.cs b/Polyfills/Generator.cs new file mode 100644 index 0000000..155035b --- /dev/null +++ b/Polyfills/Generator.cs @@ -0,0 +1,15 @@ +namespace RhoMicro.CodeAnalysis.Polyfills; + +using Generated; +using Microsoft.CodeAnalysis; + +/// +/// Generates polyfills for netstandard2 libraries. +/// +[Generator(LanguageNames.CSharp)] +public class Generator : IIncrementalGenerator +{ + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + => IncludedFileSources.RegisterToContext(context); +} diff --git a/Polyfills/Polyfills.csproj b/Polyfills/Polyfills.csproj new file mode 100644 index 0000000..7181c26 --- /dev/null +++ b/Polyfills/Polyfills.csproj @@ -0,0 +1,19 @@ + + + + netstandard2.0 + preview + enable + true + $(DefineConstants);RHOMICRO_CODEANALYSIS_POLYFILLS + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Polyfills/System/Collections/Generic/KeyValuePairExtensions.cs b/Polyfills/System/Collections/Generic/KeyValuePairExtensions.cs new file mode 100644 index 0000000..7a017d8 --- /dev/null +++ b/Polyfills/System/Collections/Generic/KeyValuePairExtensions.cs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MPL-2.0 + +namespace System.Collections.Generic; + +/// +/// Provides extensions for instances of . +/// +#if RHOMICRO_CODEANALYSIS_POLYFILLS +[RhoMicro.CodeAnalysis.IncludeFile] +#endif +internal static class KeyValuePairExtensions +{ + extension(KeyValuePair kvp) + { + /// + /// Deconstructs the into its key and value. + /// + /// + /// The key contained in the . + /// + /// + /// The value contained in the . + /// + public void Deconstruct(out TKey key, out TValue value) + { + key = kvp.Key; + value = kvp.Value; + } + } +} diff --git a/RhoMicro.CodeAnalysis.sln b/RhoMicro.CodeAnalysis.sln deleted file mode 100644 index 2a0228f..0000000 --- a/RhoMicro.CodeAnalysis.sln +++ /dev/null @@ -1,224 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.8.34309.116 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CopyToGenerator", "CopyToGenerator\CopyToGenerator.csproj", "{255A7B8C-A679-4A8F-A049-EA39EC0C248F}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestApp", "TestApp\TestApp.csproj", "{88C7D33F-6100-46C7-8941-B97C5E769A10}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{518E0314-5813-462C-AD0A-48491B32707D}" - ProjectSection(SolutionItems) = preProject - .gitattributes = .gitattributes - .gitignore = .gitignore - AutoUpdateAssemblyName.txt = AutoUpdateAssemblyName.txt - .github\workflows\buildPublish.yml = .github\workflows\buildPublish.yml - Directory.Build.props = Directory.Build.props - global.json = global.json - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnionsGenerator", "UnionsGenerator\UnionsGenerator.csproj", "{C143CDD6-2A5C-4D7E-89B2-76676A6FA70D}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Generators", "Generators", "{96BBB21A-F476-4234-97AF-D79F88CE0644}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Applications", "Applications", "{D2B8AC38-3B81-453F-A253-6D410D860D6C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UtilityGenerators", "UtilityGenerators\UtilityGenerators.csproj", "{D0545B95-3C8C-4E5F-B1ED-8AF3372BEDA3}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DslGenerator", "DslGenerator\DslGenerator.csproj", "{EC3134B3-F85E-4331-93B2-ABE5829FFF8A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DslGenerator.TestApp", "DslGenerator.TestApp\DslGenerator.TestApp.csproj", "{1F8A9A65-94CB-482E-B051-D89307A6A042}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DslGenerator.Tests", "DslGenerator.Tests\DslGenerator.Tests.csproj", "{964E486B-07D7-4771-9330-3BB3D662A8C1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IndentedStringBuilderTestApp", "IndentedStringBuilderTestApp\IndentedStringBuilderTestApp.csproj", "{08AEFD73-58E0-4EF7-A429-BE9939844D97}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocReflect.Library", "DocReflect.Library\DocReflect.Library.csproj", "{DB95ECDD-21ED-4180-B8C1-E76BE816D55B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocReflect", "DocReflect\DocReflect.csproj", "{0D39FED5-B843-454D-9CFD-EEC5CB26FC1C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Project1", "Project1\Project1.csproj", "{A12BC4A0-DCB8-4FCB-8BFA-ACD0DCD4B568}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnionsGenerator.Tests", "UnionsGenerator.Tests\UnionsGenerator.Tests.csproj", "{9743772D-6AEE-4775-AEAA-C4E58DDCE0E9}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnionsGenerator.EndToEnd.Tests", "UnionsGenerator.EndToEnd.Tests\UnionsGenerator.EndToEnd.Tests.csproj", "{374C6DDC-DDA9-450F-A449-AA7FC3985C2F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnumConstStringGenerator", "EnumConstStringGenerator\EnumConstStringGenerator.csproj", "{F6AEDF20-AA0A-4789-B8E9-3399C15EED48}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EnumStringTestApp", "EnumStringTestApp\EnumStringTestApp.csproj", "{15D08162-340C-4520-97F9-B42665D281E2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonSchemaGenerator", "JsonSchemaGenerator\JsonSchemaGenerator.csproj", "{40A4A78F-A784-4F79-9C9F-F065AD64255F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonSchemaGenerator.Tests", "JsonSchemaGenerator.Tests\JsonSchemaGenerator.Tests.csproj", "{6F3EB5CA-3DC7-4DF5-97DA-2E7C519C011F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonSchemaGenerator.Cli", "JsonSchemaGenerator.Cli\JsonSchemaGenerator.Cli.csproj", "{8076F7EE-7474-42A6-96E6-60CF266008BA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UtilityGenerators.Dev", "UtilityGenerators.Dev\UtilityGenerators.Dev.csproj", "{0E42553E-91C8-46D9-93EF-FA95EBA517C4}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UtilityGenerators.Benchmarks", "UtilityGenerators.Benchmarks\UtilityGenerators.Benchmarks.csproj", "{E9D11F2B-2694-4240-BFA8-3447FA640A35}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UtilityGenerators.Tests.E2E", "UtilityGenerators.Tests.E2E\UtilityGenerators.Tests.E2E.csproj", "{9F46450C-0927-42F5-B35E-555E0035D42A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UtilityGenerators.Tests", "UtilityGenerators.Tests\UtilityGenerators.Tests.csproj", "{03AE948B-39FF-485C-9B63-38006A5BB20F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UtilityGenerators.Library.Text.SourceTexts", "UtilityGenerators.Library.Text.SourceTexts\UtilityGenerators.Library.Text.SourceTexts.csproj", "{D5A0C9EE-C6DA-4A32-A802-EBB5981367C9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OptionsGenerator", "OptionsGenerator\OptionsGenerator.csproj", "{7253892F-8AF6-41DC-B2E5-C231E3F8E4BF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkerService1", "WorkerService1\WorkerService1.csproj", "{876F9DD3-D211-4867-B69E-60BF76946838}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OptionsGenerator.Tests", "OptionsGenerator.Tests\OptionsGenerator.Tests.csproj", "{412EC8AA-7F9B-4A54-A478-0524B791A208}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisitorGenerator", "VisitorGenerator\VisitorGenerator.csproj", "{D3BF809F-4878-470C-9897-D6C58D7068A9}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {255A7B8C-A679-4A8F-A049-EA39EC0C248F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {255A7B8C-A679-4A8F-A049-EA39EC0C248F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {255A7B8C-A679-4A8F-A049-EA39EC0C248F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {255A7B8C-A679-4A8F-A049-EA39EC0C248F}.Release|Any CPU.Build.0 = Release|Any CPU - {88C7D33F-6100-46C7-8941-B97C5E769A10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {88C7D33F-6100-46C7-8941-B97C5E769A10}.Debug|Any CPU.Build.0 = Debug|Any CPU - {88C7D33F-6100-46C7-8941-B97C5E769A10}.Release|Any CPU.ActiveCfg = Release|Any CPU - {88C7D33F-6100-46C7-8941-B97C5E769A10}.Release|Any CPU.Build.0 = Release|Any CPU - {C143CDD6-2A5C-4D7E-89B2-76676A6FA70D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C143CDD6-2A5C-4D7E-89B2-76676A6FA70D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C143CDD6-2A5C-4D7E-89B2-76676A6FA70D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C143CDD6-2A5C-4D7E-89B2-76676A6FA70D}.Release|Any CPU.Build.0 = Release|Any CPU - {D0545B95-3C8C-4E5F-B1ED-8AF3372BEDA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D0545B95-3C8C-4E5F-B1ED-8AF3372BEDA3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D0545B95-3C8C-4E5F-B1ED-8AF3372BEDA3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D0545B95-3C8C-4E5F-B1ED-8AF3372BEDA3}.Release|Any CPU.Build.0 = Release|Any CPU - {EC3134B3-F85E-4331-93B2-ABE5829FFF8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EC3134B3-F85E-4331-93B2-ABE5829FFF8A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EC3134B3-F85E-4331-93B2-ABE5829FFF8A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EC3134B3-F85E-4331-93B2-ABE5829FFF8A}.Release|Any CPU.Build.0 = Release|Any CPU - {1F8A9A65-94CB-482E-B051-D89307A6A042}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1F8A9A65-94CB-482E-B051-D89307A6A042}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1F8A9A65-94CB-482E-B051-D89307A6A042}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1F8A9A65-94CB-482E-B051-D89307A6A042}.Release|Any CPU.Build.0 = Release|Any CPU - {964E486B-07D7-4771-9330-3BB3D662A8C1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {964E486B-07D7-4771-9330-3BB3D662A8C1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {964E486B-07D7-4771-9330-3BB3D662A8C1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {964E486B-07D7-4771-9330-3BB3D662A8C1}.Release|Any CPU.Build.0 = Release|Any CPU - {08AEFD73-58E0-4EF7-A429-BE9939844D97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {08AEFD73-58E0-4EF7-A429-BE9939844D97}.Debug|Any CPU.Build.0 = Debug|Any CPU - {08AEFD73-58E0-4EF7-A429-BE9939844D97}.Release|Any CPU.ActiveCfg = Release|Any CPU - {08AEFD73-58E0-4EF7-A429-BE9939844D97}.Release|Any CPU.Build.0 = Release|Any CPU - {DB95ECDD-21ED-4180-B8C1-E76BE816D55B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DB95ECDD-21ED-4180-B8C1-E76BE816D55B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DB95ECDD-21ED-4180-B8C1-E76BE816D55B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DB95ECDD-21ED-4180-B8C1-E76BE816D55B}.Release|Any CPU.Build.0 = Release|Any CPU - {0D39FED5-B843-454D-9CFD-EEC5CB26FC1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0D39FED5-B843-454D-9CFD-EEC5CB26FC1C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0D39FED5-B843-454D-9CFD-EEC5CB26FC1C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0D39FED5-B843-454D-9CFD-EEC5CB26FC1C}.Release|Any CPU.Build.0 = Release|Any CPU - {A12BC4A0-DCB8-4FCB-8BFA-ACD0DCD4B568}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A12BC4A0-DCB8-4FCB-8BFA-ACD0DCD4B568}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A12BC4A0-DCB8-4FCB-8BFA-ACD0DCD4B568}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A12BC4A0-DCB8-4FCB-8BFA-ACD0DCD4B568}.Release|Any CPU.Build.0 = Release|Any CPU - {9743772D-6AEE-4775-AEAA-C4E58DDCE0E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9743772D-6AEE-4775-AEAA-C4E58DDCE0E9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9743772D-6AEE-4775-AEAA-C4E58DDCE0E9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9743772D-6AEE-4775-AEAA-C4E58DDCE0E9}.Release|Any CPU.Build.0 = Release|Any CPU - {374C6DDC-DDA9-450F-A449-AA7FC3985C2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {374C6DDC-DDA9-450F-A449-AA7FC3985C2F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {374C6DDC-DDA9-450F-A449-AA7FC3985C2F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {374C6DDC-DDA9-450F-A449-AA7FC3985C2F}.Release|Any CPU.Build.0 = Release|Any CPU - {F6AEDF20-AA0A-4789-B8E9-3399C15EED48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F6AEDF20-AA0A-4789-B8E9-3399C15EED48}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F6AEDF20-AA0A-4789-B8E9-3399C15EED48}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F6AEDF20-AA0A-4789-B8E9-3399C15EED48}.Release|Any CPU.Build.0 = Release|Any CPU - {15D08162-340C-4520-97F9-B42665D281E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {15D08162-340C-4520-97F9-B42665D281E2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {15D08162-340C-4520-97F9-B42665D281E2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {15D08162-340C-4520-97F9-B42665D281E2}.Release|Any CPU.Build.0 = Release|Any CPU - {40A4A78F-A784-4F79-9C9F-F065AD64255F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {40A4A78F-A784-4F79-9C9F-F065AD64255F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {40A4A78F-A784-4F79-9C9F-F065AD64255F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {40A4A78F-A784-4F79-9C9F-F065AD64255F}.Release|Any CPU.Build.0 = Release|Any CPU - {6F3EB5CA-3DC7-4DF5-97DA-2E7C519C011F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6F3EB5CA-3DC7-4DF5-97DA-2E7C519C011F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6F3EB5CA-3DC7-4DF5-97DA-2E7C519C011F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6F3EB5CA-3DC7-4DF5-97DA-2E7C519C011F}.Release|Any CPU.Build.0 = Release|Any CPU - {8076F7EE-7474-42A6-96E6-60CF266008BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8076F7EE-7474-42A6-96E6-60CF266008BA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8076F7EE-7474-42A6-96E6-60CF266008BA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8076F7EE-7474-42A6-96E6-60CF266008BA}.Release|Any CPU.Build.0 = Release|Any CPU - {0E42553E-91C8-46D9-93EF-FA95EBA517C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0E42553E-91C8-46D9-93EF-FA95EBA517C4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0E42553E-91C8-46D9-93EF-FA95EBA517C4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0E42553E-91C8-46D9-93EF-FA95EBA517C4}.Release|Any CPU.Build.0 = Release|Any CPU - {E9D11F2B-2694-4240-BFA8-3447FA640A35}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E9D11F2B-2694-4240-BFA8-3447FA640A35}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E9D11F2B-2694-4240-BFA8-3447FA640A35}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E9D11F2B-2694-4240-BFA8-3447FA640A35}.Release|Any CPU.Build.0 = Release|Any CPU - {9F46450C-0927-42F5-B35E-555E0035D42A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9F46450C-0927-42F5-B35E-555E0035D42A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9F46450C-0927-42F5-B35E-555E0035D42A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9F46450C-0927-42F5-B35E-555E0035D42A}.Release|Any CPU.Build.0 = Release|Any CPU - {03AE948B-39FF-485C-9B63-38006A5BB20F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {03AE948B-39FF-485C-9B63-38006A5BB20F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {03AE948B-39FF-485C-9B63-38006A5BB20F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {03AE948B-39FF-485C-9B63-38006A5BB20F}.Release|Any CPU.Build.0 = Release|Any CPU - {D5A0C9EE-C6DA-4A32-A802-EBB5981367C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D5A0C9EE-C6DA-4A32-A802-EBB5981367C9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D5A0C9EE-C6DA-4A32-A802-EBB5981367C9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D5A0C9EE-C6DA-4A32-A802-EBB5981367C9}.Release|Any CPU.Build.0 = Release|Any CPU - {7253892F-8AF6-41DC-B2E5-C231E3F8E4BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7253892F-8AF6-41DC-B2E5-C231E3F8E4BF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7253892F-8AF6-41DC-B2E5-C231E3F8E4BF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7253892F-8AF6-41DC-B2E5-C231E3F8E4BF}.Release|Any CPU.Build.0 = Release|Any CPU - {876F9DD3-D211-4867-B69E-60BF76946838}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {876F9DD3-D211-4867-B69E-60BF76946838}.Debug|Any CPU.Build.0 = Debug|Any CPU - {876F9DD3-D211-4867-B69E-60BF76946838}.Release|Any CPU.ActiveCfg = Release|Any CPU - {876F9DD3-D211-4867-B69E-60BF76946838}.Release|Any CPU.Build.0 = Release|Any CPU - {412EC8AA-7F9B-4A54-A478-0524B791A208}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {412EC8AA-7F9B-4A54-A478-0524B791A208}.Debug|Any CPU.Build.0 = Debug|Any CPU - {412EC8AA-7F9B-4A54-A478-0524B791A208}.Release|Any CPU.ActiveCfg = Release|Any CPU - {412EC8AA-7F9B-4A54-A478-0524B791A208}.Release|Any CPU.Build.0 = Release|Any CPU - {D3BF809F-4878-470C-9897-D6C58D7068A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {D3BF809F-4878-470C-9897-D6C58D7068A9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {D3BF809F-4878-470C-9897-D6C58D7068A9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {D3BF809F-4878-470C-9897-D6C58D7068A9}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {255A7B8C-A679-4A8F-A049-EA39EC0C248F} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - {88C7D33F-6100-46C7-8941-B97C5E769A10} = {D2B8AC38-3B81-453F-A253-6D410D860D6C} - {C143CDD6-2A5C-4D7E-89B2-76676A6FA70D} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - {D0545B95-3C8C-4E5F-B1ED-8AF3372BEDA3} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - {EC3134B3-F85E-4331-93B2-ABE5829FFF8A} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - {1F8A9A65-94CB-482E-B051-D89307A6A042} = {D2B8AC38-3B81-453F-A253-6D410D860D6C} - {964E486B-07D7-4771-9330-3BB3D662A8C1} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - {08AEFD73-58E0-4EF7-A429-BE9939844D97} = {D2B8AC38-3B81-453F-A253-6D410D860D6C} - {DB95ECDD-21ED-4180-B8C1-E76BE816D55B} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - {0D39FED5-B843-454D-9CFD-EEC5CB26FC1C} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - {A12BC4A0-DCB8-4FCB-8BFA-ACD0DCD4B568} = {D2B8AC38-3B81-453F-A253-6D410D860D6C} - {9743772D-6AEE-4775-AEAA-C4E58DDCE0E9} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - {374C6DDC-DDA9-450F-A449-AA7FC3985C2F} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - {F6AEDF20-AA0A-4789-B8E9-3399C15EED48} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - {15D08162-340C-4520-97F9-B42665D281E2} = {D2B8AC38-3B81-453F-A253-6D410D860D6C} - {40A4A78F-A784-4F79-9C9F-F065AD64255F} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - {6F3EB5CA-3DC7-4DF5-97DA-2E7C519C011F} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - {8076F7EE-7474-42A6-96E6-60CF266008BA} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - {0E42553E-91C8-46D9-93EF-FA95EBA517C4} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - {E9D11F2B-2694-4240-BFA8-3447FA640A35} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - {9F46450C-0927-42F5-B35E-555E0035D42A} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - {03AE948B-39FF-485C-9B63-38006A5BB20F} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - {D5A0C9EE-C6DA-4A32-A802-EBB5981367C9} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - {7253892F-8AF6-41DC-B2E5-C231E3F8E4BF} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - {876F9DD3-D211-4867-B69E-60BF76946838} = {D2B8AC38-3B81-453F-A253-6D410D860D6C} - {412EC8AA-7F9B-4A54-A478-0524B791A208} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - {D3BF809F-4878-470C-9897-D6C58D7068A9} = {96BBB21A-F476-4234-97AF-D79F88CE0644} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {8A219ED5-5120-4C5F-8EB4-FFF44FFCE2CF} - EndGlobalSection -EndGlobal diff --git a/RhoMicro.CodeAnalysis.slnx b/RhoMicro.CodeAnalysis.slnx new file mode 100644 index 0000000..6b0120e --- /dev/null +++ b/RhoMicro.CodeAnalysis.slnx @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/UnionsGenerator.EndToEnd.Tests/EqualityTests.cs b/UnionsGenerator.EndToEnd.Tests/EqualityTests.cs deleted file mode 100644 index 29bc48d..0000000 --- a/UnionsGenerator.EndToEnd.Tests/EqualityTests.cs +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.EndToEnd.Tests; - -using RhoMicro.CodeAnalysis; - -using System; - -public partial class EqualityTests -{ - [UnionType] - private partial class Foo - { - public Boolean Equals(Foo? foo) => true; - } - [UnionType] - private partial class Bar; -} diff --git a/UnionsGenerator.EndToEnd.Tests/FactoryTests.cs b/UnionsGenerator.EndToEnd.Tests/FactoryTests.cs deleted file mode 100644 index d3763ba..0000000 --- a/UnionsGenerator.EndToEnd.Tests/FactoryTests.cs +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.EndToEnd.Tests; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -public partial class FactoryTests -{ - [UnionType(FactoryName = "MyFactory")] - private partial class NamedFactoryUnion; - - [Fact] - public void UsesProvidedFactoryName() - { - _ = NamedFactoryUnion.MyFactory(0); - } - - [UnionType] - private partial class UnnamedFactoryUnion; - - [Fact] - public void UsesDefaultFactoryName() - { - _ = UnnamedFactoryUnion.Create(0); - } -} diff --git a/UnionsGenerator.EndToEnd.Tests/GlobalUsings.cs b/UnionsGenerator.EndToEnd.Tests/GlobalUsings.cs deleted file mode 100644 index 31ff61e..0000000 --- a/UnionsGenerator.EndToEnd.Tests/GlobalUsings.cs +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -global using Xunit; - -[assembly: RhoMicro.CodeAnalysis.UnionTypeSettings(Miscellaneous = RhoMicro.CodeAnalysis.MiscellaneousSettings.Default | RhoMicro.CodeAnalysis.MiscellaneousSettings.GenerateJsonConverter | - RhoMicro.CodeAnalysis.MiscellaneousSettings.EmitStructuralRepresentation)] diff --git a/UnionsGenerator.EndToEnd.Tests/NullableTests.cs b/UnionsGenerator.EndToEnd.Tests/NullableTests.cs deleted file mode 100644 index b3d53bc..0000000 --- a/UnionsGenerator.EndToEnd.Tests/NullableTests.cs +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.EndToEnd.Tests; - -using System; - -public partial class NullableTests -{ - [UnionType] - private readonly partial struct NullableBoolUnion; - - [UnionType(Alias = "Int", Options = UnionTypeOptions.Nullable)] - private readonly partial struct PedroUnion; - [UnionType] - private readonly partial struct PedroLongUnion; - - [Fact] - public void NullableBoolTrueFactoryCall() - { - var u = NullableBoolUnion.Create((Boolean?)true); - Assert.True(u.IsNullable_of_Boolean); - Assert.True(u.AsNullable_of_Boolean.HasValue); - Assert.True(u.AsNullable_of_Boolean.Value); - } - - [Fact] - public void NullableBoolFalseFactoryCall() - { - var u = NullableBoolUnion.Create((Boolean?)false); - Assert.True(u.IsNullable_of_Boolean); - Assert.True(u.AsNullable_of_Boolean.HasValue); - Assert.False(u.AsNullable_of_Boolean.Value); - } - - [Fact] - public void NullableBoolNullFactoryCall() - { - var u = NullableBoolUnion.Create((Boolean?)null); - Assert.True(u.IsNullable_of_Boolean); - Assert.False(u.AsNullable_of_Boolean.HasValue); - } -} diff --git a/UnionsGenerator.EndToEnd.Tests/ToStringTests.cs b/UnionsGenerator.EndToEnd.Tests/ToStringTests.cs deleted file mode 100644 index a8a519f..0000000 --- a/UnionsGenerator.EndToEnd.Tests/ToStringTests.cs +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.EndToEnd.Tests; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -public partial class ToStringTests -{ - [UnionType] - [UnionTypeSettings(ToStringSetting = ToStringSetting.None)] - private partial class NoToStringUnionType; - - [Fact] - public void UsesDefaultToString() - { - NoToStringUnionType u = "Foo"; - var expected = typeof(NoToStringUnionType).FullName; - var actual = u.ToString(); - - Assert.Equal(expected, actual); - } - - [UnionType] - private partial class CustomToStringUnionType - { - public override String ToString() => "Foo"; - } - - [Fact] - public void UsesCustomToString() - { - CustomToStringUnionType u = "Bar"; - var expected = "Foo"; - var actual = u.ToString(); - - Assert.Equal(expected, actual); - } -} diff --git a/UnionsGenerator.Tests/ConstructorTests.cs b/UnionsGenerator.Tests/ConstructorTests.cs deleted file mode 100644 index a227398..0000000 --- a/UnionsGenerator.Tests/ConstructorTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Tests; - -using System.Linq; - -public class ConstructorTests : TestBase -{ - [Fact] - public void GeneratesPrivateInterfaceAccessibilityForPublicIfInconvertible() => - TestUnionType( - """ - using RhoMicro.CodeAnalysis; - [UnionType] - [UnionType] - [UnionTypeSettings(ConstructorAccessibility = ConstructorAccessibilitySetting.PublicIfInconvertible)] - readonly partial struct IntOrString { } - """, - s => s.Constructors.All(c => - c.DeclaredAccessibility == Microsoft.CodeAnalysis.Accessibility.Private - || c.Parameters.Single().Type.TypeKind == Microsoft.CodeAnalysis.TypeKind.Interface)); - - [Fact] - public void GeneratesPrivateObjectAccessibilityForPublicIfInconvertible() => - TestUnionType( - """ - using RhoMicro.CodeAnalysis; - [UnionType] - [UnionType] - [UnionTypeSettings(ConstructorAccessibility = ConstructorAccessibilitySetting.PublicIfInconvertible)] - readonly partial struct IntOrString { } - """, - s => s.Constructors.All(c => - c.DeclaredAccessibility == Microsoft.CodeAnalysis.Accessibility.Private - || c.Parameters.Single().Type.SpecialType == Microsoft.CodeAnalysis.SpecialType.System_Object)); - - [Fact] - public void GeneratesPrivateSupertypeAccessibilityForPublicIfInconvertible() => - TestUnionType( - """ - using RhoMicro.CodeAnalysis; - class Supertype { } - [UnionType] - [UnionType] - [UnionTypeSettings(ConstructorAccessibility = ConstructorAccessibilitySetting.PublicIfInconvertible)] - partial class IntOrString : Supertype { } - """, - s => s.Constructors.All(c => - c.DeclaredAccessibility == Microsoft.CodeAnalysis.Accessibility.Private - || c.Parameters.Single().Type.Name == "Supertype"), - unionTypeName: "IntOrString"); - - [Fact] - public void GeneratesPrivateAccessibilityForPublicIfInconvertible() => - TestUnionType( - """ - using RhoMicro.CodeAnalysis; - [UnionType] - [UnionType] - [UnionTypeSettings(ConstructorAccessibility = ConstructorAccessibilitySetting.PublicIfInconvertible)] - readonly partial struct IntOrString { } - """, - s => s.Constructors.All(c => c.DeclaredAccessibility == Microsoft.CodeAnalysis.Accessibility.Private)); - - [Fact] - public void GeneratesPrivateAccessibilityForPrivate() => - TestUnionType( - """ - using RhoMicro.CodeAnalysis; - [UnionType] - [UnionType] - [UnionTypeSettings(ConstructorAccessibility = ConstructorAccessibilitySetting.Private)] - readonly partial struct IntOrString { } - """, - s => s.Constructors.All(c => c.DeclaredAccessibility == Microsoft.CodeAnalysis.Accessibility.Private)); - - [Fact] - public void GeneratesPublicAccessibilityForPublic() => - TestUnionType( - """ - using RhoMicro.CodeAnalysis; - [UnionType] - [UnionType] - [UnionTypeSettings(ConstructorAccessibility = ConstructorAccessibilitySetting.Public)] - readonly partial struct IntOrString { } - """, - s => s.Constructors.All(c => c.DeclaredAccessibility == Microsoft.CodeAnalysis.Accessibility.Public)); -} diff --git a/UnionsGenerator.Tests/ConversionTests.cs b/UnionsGenerator.Tests/ConversionTests.cs deleted file mode 100644 index ba671c0..0000000 --- a/UnionsGenerator.Tests/ConversionTests.cs +++ /dev/null @@ -1,213 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Tests; - -using System.Security.Cryptography.X509Certificates; - -using Microsoft.CodeAnalysis; - -public class ConversionTests : TestBase -{ - [Theory] - [InlineData( - """ - using RhoMicro.CodeAnalysis; - [UnionType] - readonly partial struct Union { } - """)] - [InlineData( - """ - using RhoMicro.CodeAnalysis; - [UnionType] - readonly partial struct Union { } - """)] - [InlineData( - """ - using RhoMicro.CodeAnalysis; - interface IInterface { } - [UnionType] - readonly partial struct Union { } - """)] - public void OmitsInterfaceConversions(String source) => - TestUnionType( - source, - t => {/*diagnostics due to supertype/interface conversions would fail*/ }, - unionTypeName: "Union"); - - [Theory] - [InlineData( - """ - using RhoMicro.CodeAnalysis; - using System; - [UnionType] - partial class Subset { } - [UnionType] - [UnionType] - [Relation] - partial class Union { } - """)] - public void SubsetConversions(String source) - { - AssertConversionOperator( - source, - "Union", - toPredicate: SymbolEqualityComparer.Default.Equals, - fromPredicate: (parameter, union) => parameter.Name == "Subset", - opName: WellKnownMemberNames.ImplicitConversionName); - - AssertConversionOperator( - source, - "Union", - toPredicate: (parameter, union) => parameter.Name == "Subset", - fromPredicate: SymbolEqualityComparer.Default.Equals, - opName: WellKnownMemberNames.ExplicitConversionName); - } - - [Theory] - [InlineData( - """ - using RhoMicro.CodeAnalysis; - using System; - [UnionType] - [UnionType] - [UnionType] - partial class Superset { } - [UnionType] - [UnionType] - [Relation] - partial class Union { } - """)] - public void SupersetConversions(String source) - { - AssertConversionOperator( - source, - "Union", - toPredicate: SymbolEqualityComparer.Default.Equals, - fromPredicate: (parameter, union) => parameter.Name == "Superset", - opName: WellKnownMemberNames.ExplicitConversionName); - - AssertConversionOperator( - source, - "Union", - toPredicate: (parameter, union) => parameter.Name == "Superset", - fromPredicate: SymbolEqualityComparer.Default.Equals, - opName: WellKnownMemberNames.ImplicitConversionName); - } - - [Theory] - [InlineData( - """ - using RhoMicro.CodeAnalysis; - using System; - [UnionType] - [UnionType] - partial class Congruent { } - [UnionType] - [UnionType] - [Relation] - partial class Union { } - """)] - public void CongruentConversions(String source) - { - AssertConversionOperator( - source, - "Union", - toPredicate: SymbolEqualityComparer.Default.Equals, - fromPredicate: (parameter, union) => parameter.Name == "Congruent", - opName: WellKnownMemberNames.ImplicitConversionName); - - AssertConversionOperator( - source, - "Union", - toPredicate: (parameter, union) => parameter.Name == "Congruent", - fromPredicate: SymbolEqualityComparer.Default.Equals, - opName: WellKnownMemberNames.ImplicitConversionName); - } - - [Theory] - [InlineData( - """ - using RhoMicro.CodeAnalysis; - using System; - [UnionType] - [UnionType] - partial class Intersection { } - [UnionType] - [UnionType] - [Relation] - partial class Union { } - """)] - public void IntersectionConversions(String source) - { - AssertConversionOperator( - source, - "Union", - toPredicate: SymbolEqualityComparer.Default.Equals, - fromPredicate: (parameter, union) => parameter.Name == "Intersection", - opName: WellKnownMemberNames.ExplicitConversionName); - - AssertConversionOperator( - source, - "Union", - toPredicate: (parameter, union) => parameter.Name == "Intersection", - fromPredicate: SymbolEqualityComparer.Default.Equals, - opName: WellKnownMemberNames.ExplicitConversionName); - } - - [Theory] - [InlineData( - """ - using RhoMicro.CodeAnalysis; - using System; - [UnionType] - partial class Disjunct { } - [UnionType] - [UnionType] - [Relation] - partial class Union { } - """)] - public void DisjunctConversions(String source) - { - AssertConversionOperator( - source, - "Union", - toPredicate: SymbolEqualityComparer.Default.Equals, - fromPredicate: (parameter, union) => parameter.Name == "Disjunct", - opName: WellKnownMemberNames.ExplicitConversionName, - assertExists: false); - - AssertConversionOperator( - source, - "Union", - toPredicate: (parameter, union) => parameter.Name == "Disjunct", - fromPredicate: SymbolEqualityComparer.Default.Equals, - opName: WellKnownMemberNames.ExplicitConversionName, - assertExists: false); - } - - private void AssertConversionOperator( - String source, - String unionTypeName, - Func toPredicate, - Func fromPredicate, - String opName, - Boolean assertExists = true) => - TestUnionType( - source, - t => - { - var exists = t.GetMembers() - .OfType() - .One(m => - m.IsStatic - && m.MethodKind == MethodKind.Conversion - && m.Parameters.Length == 1 - && toPredicate.Invoke(m.ReturnType, t) - && fromPredicate.Invoke(m.Parameters[0].Type, t) - && m.DeclaredAccessibility == Accessibility.Public - && ( m.Name == opName || !assertExists )); - - Assert.Equal(exists, assertExists); - }, - unionTypeName); -} diff --git a/UnionsGenerator.Tests/DiagnosticsLevelTests.cs b/UnionsGenerator.Tests/DiagnosticsLevelTests.cs deleted file mode 100644 index 5ff54dc..0000000 --- a/UnionsGenerator.Tests/DiagnosticsLevelTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Tests; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -using Basic.Reference.Assemblies; - -using Microsoft.CodeAnalysis; - -public class DiagnosticsLevelTests : TestBase -{ - public DiagnosticsLevelTests() : base(Net80.References.All) { } - - [Theory] - [InlineData( - """ - using RhoMicro.CodeAnalysis; - using System; - - [UnionType(Storage = StorageOption.Value)] - [UnionTypeSettings(Miscellaneous = MiscellaneousSettings.EmitStructuralRepresentation, DiagnosticsLevel = DiagnosticsLevelSettings.Error)] - readonly partial struct GetUserResult { } - - sealed record User(String Name) { } - - enum ErrorCode - { - NotFound, - Unauthorized - } - - readonly record struct MultipleUsersError(Int32 Count) { } - """, "GetUserResult")] - public void OmitsWarningDiagnostics(String source, String unionTypeName) => - TestDiagnostics(source, async compilation => - { - var diagnostics = await compilation.GetAnalyzerDiagnosticsAsync(); - Assert.All(diagnostics, d => Assert.Equal(DiagnosticSeverity.Error, d.Severity)); - }); -} diff --git a/UnionsGenerator.Tests/EnumerableExtensions.cs b/UnionsGenerator.Tests/EnumerableExtensions.cs deleted file mode 100644 index 5283a08..0000000 --- a/UnionsGenerator.Tests/EnumerableExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Tests; - -using Microsoft.CodeAnalysis; - -internal static class EnumerableExtensions -{ - public static Boolean None(this IEnumerable enumeration) - => !enumeration.Any(); - public static Boolean None(this IEnumerable enumeration, Func predicate) - => !enumeration.Any(predicate); - public static Boolean One(this IEnumerable enumeration, Func predicate) - => enumeration.Where(predicate).Count() == 1; -} diff --git a/UnionsGenerator.Tests/JsonConverterTests.cs b/UnionsGenerator.Tests/JsonConverterTests.cs deleted file mode 100644 index dbeb702..0000000 --- a/UnionsGenerator.Tests/JsonConverterTests.cs +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Tests; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -using Basic.Reference.Assemblies; - -using Microsoft.CodeAnalysis; - -public class JsonConverterTests() : TestBase(Net80.References.All) -{ - [Theory] - [InlineData( - """ - using RhoMicro.CodeAnalysis; - using System; - - [UnionType] - partial class Union { } - """)] - [InlineData( - """ - using RhoMicro.CodeAnalysis; - using System; - - [UnionType] - [UnionTypeSettings(Miscellaneous = MiscellaneousSettings.Default)] - partial class Union { } - """)] - [InlineData( - """ - using RhoMicro.CodeAnalysis; - using System; - - [UnionType] - [UnionTypeSettings(Miscellaneous = MiscellaneousSettings.None)] - partial class Union { } - """)] - [InlineData( - """ - using RhoMicro.CodeAnalysis; - using System; - - [UnionType] - [UnionTypeSettings] - partial class Union { } - """)] - [InlineData( - """ - using RhoMicro.CodeAnalysis; - using System; - - [assembly: UnionTypeSettings(Miscellaneous = MiscellaneousSettings.None)] - - [UnionType] - partial class Union { } - """)] - [InlineData( - """ - using RhoMicro.CodeAnalysis; - using System; - - [assembly: UnionTypeSettings(Miscellaneous = MiscellaneousSettings.Default)] - - [UnionType] - partial class Union { } - """)] - [InlineData( - """ - using RhoMicro.CodeAnalysis; - using System; - - [assembly: UnionTypeSettings] - - [UnionType] - partial class Union { } - """)] - public void OmitsJsonConverter(String source) => - TestUnionType(source, t => - { - var omitted = t.GetTypeMembers().None(t => - t.Name == "JsonConverter"); - - Assert.True(omitted); - }); - [Theory] - [InlineData( - """ - using RhoMicro.CodeAnalysis; - using System; - - [UnionType] - [UnionTypeSettings(Miscellaneous = MiscellaneousSettings.GenerateJsonConverter)] - partial class Union { } - """)] - [InlineData( - """ - using RhoMicro.CodeAnalysis; - using System; - - [assembly: UnionTypeSettings(Miscellaneous = MiscellaneousSettings.GenerateJsonConverter)] - - [UnionType] - partial class Union { } - """)] - public void IncludesJsonConverter(String source) => - TestUnionType(source, t => - { - var included = t.GetTypeMembers().One(t => - t.Name == "JsonConverter"); - - Assert.True(included); - }); -} diff --git a/UnionsGenerator.Tests/NullableTests.cs b/UnionsGenerator.Tests/NullableTests.cs deleted file mode 100644 index 32a22e4..0000000 --- a/UnionsGenerator.Tests/NullableTests.cs +++ /dev/null @@ -1,123 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Tests; -using System; -using System.Linq; - -using Microsoft.CodeAnalysis; - -public class NullableTests : TestBase -{ - [Fact] - public void AllowsForNullableValueType() - { - TestUnionType( - """ - using System; - using RhoMicro.CodeAnalysis; - - [UnionType>] - partial struct NullableBoolUnion { } - """, - s => { }); - } - - [Theory] - [InlineData(new[] { "Foo", "Bar" }, new String[] { }, "IsFoo", true, "AsFoo")] - [InlineData(new[] { "Foo", "Bar" }, new String[] { }, "IsBar", true, "AsBar")] - [InlineData(new[] { "Foo", "Bar" }, new String[] { }, "IsFoo", false, "AsBar")] - [InlineData(new[] { "Foo", "Bar" }, new String[] { }, "IsBar", false, "AsFoo")] - - [InlineData(new[] { "Foo" }, new String[] { "Bar" }, "IsFoo", true, "AsFoo")] - [InlineData(new[] { "Foo" }, new String[] { "Bar" }, "IsBar", true)] - [InlineData(new[] { "Foo" }, new String[] { "Bar" }, "IsFoo", false)] - [InlineData(new[] { "Foo" }, new String[] { "Bar" }, "IsBar", false, "AsFoo")] - - [InlineData(new String[] { }, new String[] { "Foo", "Bar" }, "IsFoo", true)] - [InlineData(new String[] { }, new String[] { "Foo", "Bar" }, "IsBar", true)] - [InlineData(new String[] { }, new String[] { "Foo", "Bar" }, "IsFoo", false)] - [InlineData(new String[] { }, new String[] { "Foo", "Bar" }, "IsBar", false)] - - [InlineData(new[] { "Foo", "Bar", "Baz" }, new String[] { }, "IsFoo", true, "AsFoo")] - [InlineData(new[] { "Foo", "Bar", "Baz" }, new String[] { }, "IsBar", true, "AsBar")] - [InlineData(new[] { "Foo", "Bar", "Baz" }, new String[] { }, "IsBaz", true, "AsBaz")] - [InlineData(new[] { "Foo", "Bar", "Baz" }, new String[] { }, "IsFoo", false)] - [InlineData(new[] { "Foo", "Bar", "Baz" }, new String[] { }, "IsBar", false)] - [InlineData(new[] { "Foo", "Bar", "Baz" }, new String[] { }, "IsBaz", false)] - - [InlineData(new[] { "Foo", "Bar" }, new String[] { "Baz" }, "IsFoo", true, "AsFoo")] - [InlineData(new[] { "Foo", "Bar" }, new String[] { "Baz" }, "IsBar", true, "AsBar")] - [InlineData(new[] { "Foo", "Bar" }, new String[] { "Baz" }, "IsBaz", true)] - [InlineData(new[] { "Foo", "Bar" }, new String[] { "Baz" }, "IsFoo", false)] - [InlineData(new[] { "Foo", "Bar" }, new String[] { "Baz" }, "IsBar", false)] - [InlineData(new[] { "Foo", "Bar" }, new String[] { "Baz" }, "IsBaz", false)] - - [InlineData(new[] { "Foo" }, new String[] { "Baz", "Bar" }, "IsFoo", true, "AsFoo")] - [InlineData(new[] { "Foo" }, new String[] { "Baz", "Bar" }, "IsBar", true)] - [InlineData(new[] { "Foo" }, new String[] { "Baz", "Bar" }, "IsBaz", true)] - [InlineData(new[] { "Foo" }, new String[] { "Baz", "Bar" }, "IsFoo", false)] - [InlineData(new[] { "Foo" }, new String[] { "Baz", "Bar" }, "IsBar", false)] - [InlineData(new[] { "Foo" }, new String[] { "Baz", "Bar" }, "IsBaz", false)] - - [InlineData(new String[] { }, new String[] { "Foo", "Bar", "Baz" }, "IsFoo", true)] - [InlineData(new String[] { }, new String[] { "Foo", "Bar", "Baz" }, "IsBar", true)] - [InlineData(new String[] { }, new String[] { "Foo", "Bar", "Baz" }, "IsBaz", true)] - [InlineData(new String[] { }, new String[] { "Foo", "Bar", "Baz" }, "IsFoo", false)] - [InlineData(new String[] { }, new String[] { "Foo", "Bar", "Baz" }, "IsBar", false)] - [InlineData(new String[] { }, new String[] { "Foo", "Bar", "Baz" }, "IsBaz", false)] - public void AnnotatesWithMemberNotNullWhenAttribute( - String[] classNames, - String[] structNames, - String propertyName, - Boolean expectedCondition, - params String[] expectedMemberNames) - { - TestUnionType( - $$""" - using RhoMicro.CodeAnalysis; - - {{String.Join('\n', classNames.Select(n => $"class {n.TrimEnd('?')} {{}}"))}} - {{String.Join('\n', structNames.Select(n => $"struct {n} {{}}"))}} - - [UnionType<{{String.Join(',', structNames.Concat(classNames.Select(n => n.TrimEnd('?'))))}}>] - partial struct Union { } - """, - s => - { - var actualMatch = s.GetMembers() - .OfType() - .Single(p => p.Name == propertyName) - .GetAttributes() - .Where(a => - a.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == - "global::System.Diagnostics.CodeAnalysis.MemberNotNullWhenAttribute") - .Select(a => a.ConstructorArguments) - .Select(args => - { - var result = args.Length == expectedMemberNames.Length + 1 - && args[0] is - { - Kind: TypedConstantKind.Primitive, - Value: Boolean actualCondition - } - && actualCondition == expectedCondition - && args.Skip(1) - .Zip(expectedMemberNames) - .All(t => t.First is - { - Kind: TypedConstantKind.Primitive, - Value: String actualMemberName - } && actualMemberName == t.Second); - - return result; - }) - .Where(v => v) - .SingleOrDefault(); - - var expectedMatch = expectedMemberNames is [.., { }]; - - Assert.Equal(expectedMatch, actualMatch); - }, - "Union"); - } -} diff --git a/UnionsGenerator.Tests/TestBase.cs b/UnionsGenerator.Tests/TestBase.cs deleted file mode 100644 index dce6629..0000000 --- a/UnionsGenerator.Tests/TestBase.cs +++ /dev/null @@ -1,212 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Tests; - -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -using Basic.Reference.Assemblies; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Generators; - -using Microsoft.CodeAnalysis.Diagnostics; - -using System.Collections.Immutable; - -/// -/// Base class for tests verifying outputs. -/// -public abstract class TestBase -{ - protected TestBase() : this(NetStandard20.References.All.ToArray()) { } - - protected TestBase(IEnumerable references) => - _references = [.. references]; - - private readonly MetadataReference[] _references; - - //requiring >= C#11 due to file scoped modifiers - private const LanguageVersion _targetLanguageVersion = LanguageVersion.CSharp11; - - private static readonly CSharpParseOptions _parseOptions = - new(languageVersion: _targetLanguageVersion, - documentationMode: DocumentationMode.Diagnose, - kind: SourceCodeKind.Regular); - - /// - /// Invokes an assertion on the union type implementation generated from a source. - /// - /// - /// - /// - public void TestUnionType(String source, Action assertion, String? unionTypeName = null) - { - _ = assertion ?? throw new ArgumentNullException(nameof(assertion)); - - Compilation compilation = CreateCompilation(source, out var sourceTree); - _ = RunGenerator(ref compilation); - var declaration = sourceTree.GetRoot() - .DescendantNodesAndSelf() - .OfType() - .SingleOrDefault(d => unionTypeName == null || d.Identifier.Text == unionTypeName); - Assert.NotNull(declaration); - var symbol = compilation.GetSemanticModel(sourceTree) - .GetDeclaredSymbol(declaration); - Assert.NotNull(symbol); - assertion.Invoke(symbol!); - } - - /// - /// Invokes an assertion on the result of running the generator once on a source. - /// - /// - /// - public void TestDriverResult(String source, Action assertion) - { - _ = assertion ?? throw new ArgumentNullException(nameof(assertion)); - - Compilation compilation = CreateCompilation(source, out var _); - var result = RunGenerator(ref compilation); - assertion.Invoke(result); - } - - public Task TestDiagnostics(String source, Func assertion) - { - _ = assertion ?? throw new ArgumentNullException(nameof(assertion)); - - var compilation = CreateCompilation(source, out _); - var compilationWithDiagnostics = AttachAnalyzer(compilation); - return assertion.Invoke(compilationWithDiagnostics); - } - - private CompilationWithAnalyzers AttachAnalyzer(Compilation compilation) - { - var result = compilation.WithAnalyzers([(DiagnosticAnalyzer)new Analyzers.Analyzer()]); - - return result; - } - - private GeneratorDriverRunResult RunGenerator(ref Compilation compilation) - { - var generator = new UnionsGenerator(); - - var driver = CSharpGeneratorDriver.Create(generator) - .WithUpdatedParseOptions(_parseOptions); - - // Run the generation pass - // (Note: the generator driver itself is immutable, and all calls return an updated version of the driver that you should use for subsequent calls) - driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out compilation, out var diagnostics); - - // We can now assert things about the resulting compilation: - Assert.Empty(diagnostics); // there were no diagnostics created by the generators - var aggregateDiagnostics = compilation.GetDiagnostics(); - Assert.Empty(aggregateDiagnostics); // verify the compilation with the added source has no diagnostics - - // Or we can look at the results directly: - var result = driver.GetRunResult(); - - // The runResult contains the combined results of all generators passed to the driver - Assert.Empty(result.Diagnostics); - - return result; - } - - private CSharpCompilation CreateCompilation(String source, out SyntaxTree sourceTree) - { - var options = CreateCompilationOptions(); - sourceTree = CSharpSyntaxTree.ParseText(source, _parseOptions); - var attributeTree = CSharpSyntaxTree.ParseText( - """ - // - #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; } - } - [global::System.AttributeUsage( - global::System.AttributeTargets.Method | - global::System.AttributeTargets.Property, - Inherited = false, AllowMultiple = true)] - [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] - internal sealed class MemberNotNullWhenAttribute : global::System.Attribute - { - /// - /// Initializes the attribute with the specified return value condition and a field or property member. - /// - /// The return value condition. If the method returns this value, the associated parameter will not be null. - /// The field or property member that is promised to be not-null. - public MemberNotNullWhenAttribute(bool returnValue, string member) - { - ReturnValue = returnValue; - Members = new[] { member }; - } - - /// - /// Initializes the attribute with the specified return value condition and list of field and property members. - /// - /// The return value condition. If the method returns this value, the associated parameter will not be null. - /// The list of field and property members that are promised to be not-null. - public MemberNotNullWhenAttribute(bool returnValue, params string[] members) - { - ReturnValue = returnValue; - Members = members; - } - - /// - /// Gets the return value condition. - /// - public bool ReturnValue { get; } - - /// - /// Gets field or property member names. - /// - public string[] Members { get; } - } - } - """, _parseOptions); - - var result = CSharpCompilation.Create( - assemblyName: null, - syntaxTrees: [sourceTree, attributeTree], - references: [.. _references], - options: options); - - return result; - } - - private static CSharpCompilationOptions CreateCompilationOptions() - { - String[] args = ["/warnaserror"]; -#pragma warning disable RS1035 // Do not use APIs banned for analyzers (not an analyzer????) - var commandLineArguments = CSharpCommandLineParser.Default.Parse(args, - baseDirectory: Environment.CurrentDirectory, sdkDirectory: Environment.CurrentDirectory); -#pragma warning restore RS1035 // Do not use APIs banned for analyzers - var result = commandLineArguments.CompilationOptions - .WithOutputKind(OutputKind.DynamicallyLinkedLibrary); - - return result; - } -} diff --git a/UnionsGenerator.Tests/UnionsGenerator.Tests.csproj b/UnionsGenerator.Tests/UnionsGenerator.Tests.csproj deleted file mode 100644 index 99e1af2..0000000 --- a/UnionsGenerator.Tests/UnionsGenerator.Tests.csproj +++ /dev/null @@ -1,33 +0,0 @@ - - - - net8.0 - RhoMicro.CodeAnalysis.UnionsGenerator.Tests - false - true - - - - $(DefineConstants);UNIONS_GENERATOR - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - diff --git a/UnionsGenerator/Analyzers/Analyzer.cs b/UnionsGenerator/Analyzers/Analyzer.cs deleted file mode 100644 index 5bcebe8..0000000 --- a/UnionsGenerator/Analyzers/Analyzer.cs +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Analyzers; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Diagnostics; - -using RhoMicro.CodeAnalysis.Library; -using RhoMicro.CodeAnalysis.UnionsGenerator.Models; -using RhoMicro.CodeAnalysis.UnionsGenerator.Utils; - -using System.Collections.Immutable; - -[DiagnosticAnalyzer(LanguageNames.CSharp)] -public sealed class Analyzer : DiagnosticAnalyzer -{ - public override void Initialize(AnalysisContext context) - { - _ = context ?? throw new ArgumentNullException(nameof(context)); - - context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); - context.EnableConcurrentExecution(); - context.RegisterSymbolStartAction(ctx => - { - ctx.CancellationToken.ThrowIfCancellationRequested(); - if(ctx is not { IsGeneratedCode: false, Symbol: INamedTypeSymbol target } || !Qualifications.IsUnionTypeSymbol(target, ctx.CancellationToken)) - return; - - ctx.RegisterSymbolEndAction(ctx => - { - ctx.CancellationToken.ThrowIfCancellationRequested(); - if(ctx is not { IsGeneratedCode: false, Symbol: INamedTypeSymbol target } || !Qualifications.IsUnionTypeSymbol(target, ctx.CancellationToken)) - return; - - var model = UnionTypeModel.Create(target, ctx.CancellationToken); - - if(model.Settings.DiagnosticsLevel == DiagnosticsLevelSettings.None) - return; - - var accumulator = DiagnosticsAccumulator.Create(model) - .DiagnoseNonHiddenSeverities(); - - if(model.Settings.DiagnosticsLevel != DiagnosticsLevelSettings.None) - { - if(model.Settings.DiagnosticsLevel.HasFlag(DiagnosticsLevelSettings.Info)) - { - accumulator = accumulator.ReportSeverity(DiagnosticSeverity.Info); - } - - if(model.Settings.DiagnosticsLevel.HasFlag(DiagnosticsLevelSettings.Warning)) - { - accumulator = accumulator.ReportSeverity(DiagnosticSeverity.Warning); - } - - if(model.Settings.DiagnosticsLevel.HasFlag(DiagnosticsLevelSettings.Error)) - { - accumulator = accumulator.ReportSeverity(DiagnosticSeverity.Error); - } - } - - accumulator.Receive(Providers.All, ctx.CancellationToken) - .ReportDiagnostics(ctx.ReportDiagnostic); - }); - }, SymbolKind.NamedType); - } - - public override ImmutableArray SupportedDiagnostics { get; } = Diagnostics.Descriptors.ToImmutableArray(); -} diff --git a/UnionsGenerator/Analyzers/Diagnostics.cs b/UnionsGenerator/Analyzers/Diagnostics.cs deleted file mode 100644 index 879964e..0000000 --- a/UnionsGenerator/Analyzers/Diagnostics.cs +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Analyzers; - -using Microsoft.CodeAnalysis; - -using System; - -internal static partial class Diagnostics -{ - private const String _category = "RhoMicro.CodeAnalysis.UnionsGenerator"; - - //IMPORTANT: append new values at the end to preserve error codes - private enum Id - { - DuplicateUnionTypeAttributes = 1, - GeneratorException = 2, - //MissingUnionTypeAttribute = 3, - //NonPartialDeclaration = 4, - StaticTarget = 5, - RecordTarget = 6, - TooManyTypes = 7, - ImplicitConversionOptionOnNonSolitary = 8, - //ImplicitConversionOptionOnSolitary = 9, - InvalidAttributeTarget = 10, - AliasCollision = 11, - UnionTypeSettingsOnNonUnionType = 12, - RepresentableTypeIsSupertype = 13, - RepresentableTypeIsInterface = 14, - ReservedGenericParameterName = 15, - //UnknownGenericParameterName = 16, - PossibleBoxingStrategy = 17, - BoxingStrategy = 18, - PossibleTleStrategy = 19, - TleStrategy = 20, - SmallGenericUnion = 21, - GenericViolationStrategy = 22, - GenericRelation = 23, - BidirectionalRelation = 24, - DuplicateRelation = 25, - SelfReference = 26, - Inheritance = 27, - NullableOptionOnValueType = 28 - } - public static Diagnostic NullableOptionOnValueType(Location location, String alias) => - Create(Id.NullableOptionOnValueType, location, alias); - public static Diagnostic Inheritance(Location location) => - Create(Id.Inheritance, location); - public static Diagnostic SelfReference(Location location) => - Create(Id.SelfReference, location); - public static Diagnostic DuplicateRelation(Location location, String relationName) => - Create(Id.DuplicateRelation, location, relationName); - public static Diagnostic BidirectionalRelation(Location location, String relationName) => - Create(Id.BidirectionalRelation, location, relationName); - public static Diagnostic GenericRelation(Location location) => - Create(Id.GenericRelation, location); - public static Diagnostic GenericViolationStrategy(Location location, String typeName) => - Create(Id.GenericViolationStrategy, location, typeName); - public static Diagnostic SmallGenericUnion(Location location) => - Create(Id.SmallGenericUnion, location); - public static Diagnostic PossibleBoxingStrategy(Location location, String typeName) => - Create(Id.PossibleBoxingStrategy, location, typeName); - public static Diagnostic BoxingStrategy(Location location, String typeName) => - Create(Id.BoxingStrategy, location, typeName); - public static Diagnostic PossibleTleStrategy(Location location, String typeName) => - Create(Id.PossibleTleStrategy, location, typeName); - public static Diagnostic TleStrategy(Location location, String typeName) => - Create(Id.TleStrategy, location, typeName); - //public static Diagnostic UnknownGenericParameterName(Location location, String name) => - // Create(Id.UnknownGenericParameterName, location, name); - public static Diagnostic ReservedGenericParameterName(Location location, String name) => - Create(Id.ReservedGenericParameterName, location, name); - public static Diagnostic RepresentableTypeIsInterface(Location location, String representableTypeName) => - Create(Id.RepresentableTypeIsInterface, location, representableTypeName); - public static Diagnostic RepresentableTypeIsSupertype(Location location, String representableTypeName) => - Create(Id.RepresentableTypeIsSupertype, location, representableTypeName); - public static Diagnostic UnionTypeSettingsOnNonUnionType(Location location) => - Create(Id.UnionTypeSettingsOnNonUnionType, location); - public static Diagnostic AliasCollision(Location location, String representableTypeName) => - Create(Id.AliasCollision, location, representableTypeName); - public static Diagnostic InvalidAttributeTarget(Location location) => - Create(Id.InvalidAttributeTarget, location); - public static Diagnostic TooManyTypes(Location location) => - Create(Id.TooManyTypes, location); - public static Diagnostic StaticTarget(Location location) => - Create(Id.StaticTarget, location); - public static Diagnostic RecordTarget(Location location) => - Create(Id.RecordTarget, location); - //public static Diagnostic NonPartialDeclaration(Location location) => - // Create(Id.NonPartialDeclaration, location); - public static Diagnostic GeneratorException(Exception exception) => - Create(Id.GeneratorException, Location.None, exception.Message); - public static Diagnostic DuplicateUnionTypeAttributes(Location location, String unionTypeName) => - Create(Id.DuplicateUnionTypeAttributes, location, unionTypeName); - public static Diagnostic ImplicitConversionOptionOnNonSolitary(Location location) => - Create(Id.ImplicitConversionOptionOnNonSolitary, location); - //public static Diagnostic ImplicitConversionOptionOnSolitary(String unionTypeName, String representableTypeName, Location location) => - // Create(Id.ImplicitConversionOptionOnSolitary, location, $"ISuperset<{representableTypeName}, {unionTypeName}>"); -} diff --git a/UnionsGenerator/Analyzers/DiagnosticsStrings.cs b/UnionsGenerator/Analyzers/DiagnosticsStrings.cs deleted file mode 100644 index 820e49d..0000000 --- a/UnionsGenerator/Analyzers/DiagnosticsStrings.cs +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Analyzers; - -using Microsoft.CodeAnalysis; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Utils; - -using System; -using System.Collections.Generic; -using System.Linq; - -internal static partial class Diagnostics -{ - private readonly struct DiagnosticInfo(String title, String message, DiagnosticSeverity severity) : IEquatable - { - public readonly String Title = title; - public readonly String Message = message; - public readonly DiagnosticSeverity Severity = severity; - - public override Boolean Equals(Object? obj) => obj is DiagnosticInfo item && Equals(item); - public Boolean Equals(DiagnosticInfo other) => Title == other.Title && Message == other.Message && Severity == other.Severity; - - public override Int32 GetHashCode() - { - var hashCode = -940627307; - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Title); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Message); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Severity); - return hashCode; - } - - public static Boolean operator ==(DiagnosticInfo left, DiagnosticInfo right) => left.Equals(right); - public static Boolean operator !=(DiagnosticInfo left, DiagnosticInfo right) => !( left == right ); - } - - private static readonly IReadOnlyDictionary _infos = new Dictionary() - { -{Id.AliasCollision , new DiagnosticInfo(severity: DiagnosticSeverity.Error,message:"An alias collision has been detected for the representable type `{0}`. Make sure that for colliding type names (excluding generic arguments) you provide a unique alias. If no explicit alias is provided, the simple type name of the representable type will be used, e.g.: `List` will be used for variables of the `List` type. Aliae are used for parameter, variable, enum and field names.",title:"Alias Collision")}, -{Id.BoxingStrategy , new DiagnosticInfo(severity: DiagnosticSeverity.Warning,message:"Boxing will occur for the representable type {0}. Consider using a different storage option.",title:"Boxing Storage Detected")}, -{Id.DuplicateUnionTypeAttributes , new DiagnosticInfo(severity: DiagnosticSeverity.Error,message:"Union types may not be declared using duplicate 'UnionType' attributes of the same representable type ({0}).",title:"Duplicate Union Type Declaration")}, -{Id.TooManyTypes, new DiagnosticInfo(severity: DiagnosticSeverity.Error,message:$"Union types may represent no more that {Qualifications.MaxRepresentableTypesCount} types.",title:"Too many Union Types")}, -{Id.GeneratorException , new DiagnosticInfo(severity: DiagnosticSeverity.Error,message:"The generator encountered an unexpected error: {0}",title:"Unexpected Generator Error")}, -{Id.GenericViolationStrategy , new DiagnosticInfo(severity: DiagnosticSeverity.Warning,message:"The selected storage option for {0} was ignored because the target union type is generic.",title:"Generic Storage Violation Detected")}, -{Id.ImplicitConversionOptionOnNonSolitary , new DiagnosticInfo(severity: DiagnosticSeverity.Warning,message:"The `ImplicitConversionIfSolitary` option will be ignored because the target union type may represent more than one type.",title:"Union Type Option Ignored")}, -//{Id.ImplicitConversionOptionOnSolitary , new DiagnosticInfo(severity: DiagnosticSeverity.Info,message:"The interface implementation for `{0}` was omitted because the `ImplicitConversionIfSolitary` option was used.",title:"Omitting Interface Implementation")}, -{Id.InvalidAttributeTarget , new DiagnosticInfo(severity: DiagnosticSeverity.Error,message:"Only type declarations may be annotated with union type attributes.",title:"Invalid Attribute Target")}, -//{Id.MissingUnionTypeAttribute , new DiagnosticInfo(severity: DiagnosticSeverity.Error,message:"Union types must be declared using at least one instance of the 'UnionType' attribute.",title:"Missing 'UnionType' Attribute")}, -//{Id.NonPartialDeclaration , new DiagnosticInfo(severity: DiagnosticSeverity.Error,message:"Union types must be declared using the 'partial' keyword.",title:"Nonpartial Union Declaration")}, -{Id.PossibleBoxingStrategy , new DiagnosticInfo(severity: DiagnosticSeverity.Warning,message:"Boxing may occur for the representable type {0}. Consider constraining it to either `class` or `struct` in order to help the generator emit an efficient implementation. Alternatively, use the `StorageOption.Field` option to generate dedicated field for the type.",title:"Possible Boxing Storage Detected")}, -{Id.PossibleTleStrategy , new DiagnosticInfo(severity: DiagnosticSeverity.Warning,message:"The selected storage option for {0} was ignored because it could cause a `TypeLoadException` to be thrown.",title:"Possible TypeLoadException Detected")}, -{Id.RecordTarget , new DiagnosticInfo(severity: DiagnosticSeverity.Error,message:"Union types may not be declared as 'record' types.",title:"Record Union Type Declaration")}, -{Id.RepresentableTypeIsInterface , new DiagnosticInfo(severity: DiagnosticSeverity.Info,message:"No conversion operators will be generated for the representable type {0} because it is an interface.",title:"Conversion Operators Omitted")}, -{Id.RepresentableTypeIsSupertype , new DiagnosticInfo(severity: DiagnosticSeverity.Info,message:"No conversion operators will be generated for the representable type {0} because it is a supertype of the target union type.",title:"Conversion Operators Omitted")}, -{Id.ReservedGenericParameterName , new DiagnosticInfo(severity: DiagnosticSeverity.Error,message:"The targeted union type contains a generic parameter named `{0}` which is reserved for generated code. Either change its name or use the `UnionTypeSettings` attribute to set generated generic type parameter names.",title:"Reserved Generic Parameter Name")}, -{Id.SmallGenericUnion , new DiagnosticInfo(severity: DiagnosticSeverity.Info,message:"The small layout setting was ignored because the target union type is generic.",title:"Ignoring Small Layout")}, -{Id.StaticTarget , new DiagnosticInfo(severity: DiagnosticSeverity.Error,message:"Union types may not be declared using the 'static' modifier.",title:"Static Union Type Declaration")}, -{Id.TleStrategy , new DiagnosticInfo(severity: DiagnosticSeverity.Warning,message:"The selected storage option for {0} was ignored because it would cause a `TypeLoadException` to be thrown.",title:"TypeLoadException Prevented")}, -{Id.UnionTypeSettingsOnNonUnionType , new DiagnosticInfo(severity: DiagnosticSeverity.Warning,message:"The union type settings attribute will be ignored because the target type is not a union type.",title:"Union Type Settings Ignored")}, -//{Id.UnknownGenericParameterName , new DiagnosticInfo(severity: DiagnosticSeverity.Error,message:"The targeted union type contains an unknown parameter name `{0}`. It could not be located in the type parameter list.",title:"Unknown Generic Parameter Name") }, -{Id.GenericRelation, new DiagnosticInfo(severity: DiagnosticSeverity.Error,message:"Relations may not be declared between generic types.",title:"Generic Relation") }, -{Id.BidirectionalRelation, new DiagnosticInfo(severity: DiagnosticSeverity.Error,message:"The bidirectional relation with {0} will be ignored.",title:"Bidirectional Relation") }, -{Id.DuplicateRelation, new DiagnosticInfo(severity: DiagnosticSeverity.Warning,message:"The duplicate relation with {0} will be ignored.",title:"Duplicate Relation") }, -{Id.SelfReference, new DiagnosticInfo(severity: DiagnosticSeverity.Error,message:"Union types may not reference themselves.",title:"Self Reference") }, -{Id.Inheritance, new DiagnosticInfo(severity: DiagnosticSeverity.Error,message:"Inheritance on union types is not supported yet. Try working around this restriction by adding the base type to the unions list of representable types.",title:"Inheritance") }, - {Id.NullableOptionOnValueType, new DiagnosticInfo(severity: DiagnosticSeverity.Warning, message:"The nullable option used for the representable type {0} will be ignored because the representable type is a value type. If you intended to use a nullable value type, use the '?' annotation on the attribute type argument. Alternatively, use the 'System.Nullable<>' type.", title:"Nullable Option On Value Type") } -}; - - public static readonly IReadOnlyList Descriptors = - Enum.GetValues(typeof(Id)) - .OfType() - .Select(id => new DiagnosticDescriptor( - $"RUG{(Int32)id:0000}", - _infos[id].Title, - _infos[id].Message, - _category, - _infos[id].Severity, - true)) - .ToList(); - private static readonly Dictionary _idDescriptorMap = - Enum.GetValues(typeof(Id)) - .OfType() - .Select(id => (Id: id, Descriptor: new DiagnosticDescriptor( - $"RUG{(Int32)id:0000}", - _infos[id].Title, - _infos[id].Message, - _category, - _infos[id].Severity, - true))) - .ToDictionary(t => t.Id, t => t.Descriptor); - - private static Diagnostic Create( - Id id, - Location location, - params Object[] messageArgs) => - Diagnostic.Create( - _idDescriptorMap[id], - location, - messageArgs); -} diff --git a/UnionsGenerator/Analyzers/Extensions.cs b/UnionsGenerator/Analyzers/Extensions.cs deleted file mode 100644 index 84cd0a4..0000000 --- a/UnionsGenerator/Analyzers/Extensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Analyzers; -using System; -using System.Collections.Generic; -using System.Text; - -using Microsoft.CodeAnalysis; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -internal static class Extensions -{ - public static void AddRange(this IDiagnosticsAccumulator diagnostics, IEnumerable range) - { - foreach(var diagnostic in range) - _ = diagnostics.Add(diagnostic); - } -} diff --git a/UnionsGenerator/Analyzers/Providers.cs b/UnionsGenerator/Analyzers/Providers.cs deleted file mode 100644 index cdb587f..0000000 --- a/UnionsGenerator/Analyzers/Providers.cs +++ /dev/null @@ -1,342 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Analyzers; - -using System.Xml.Linq; - -using Microsoft.CodeAnalysis; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Models; -using RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Storage; -using RhoMicro.CodeAnalysis.UnionsGenerator.Utils; - -internal static class Providers -{ - private static void Add( - UnionTypeModel model, - IDiagnosticsAccumulator diagnostics, - Func factory) => diagnostics.AddRange(model.Locations.Value.Select(factory)); - private static void Add( - UnionTypeModel model, - IDiagnosticsAccumulator diagnostics, - Func> factory) => diagnostics.AddRange(model.Locations.Value.SelectMany(factory)); - public static readonly IDiagnosticProvider NullableOptionOnValueType = - DiagnosticProvider.Create(static (model, diagnostics) => - { - var nullableValueTypes = model.RepresentableTypes - .Where(t => t.Signature.IsNullableAnnotated); - - if(!nullableValueTypes.Any()) - return; - - Add(model, diagnostics, - l => nullableValueTypes.Select(t => Diagnostics.NullableOptionOnValueType(l, t.Alias))); - }); - public static readonly IDiagnosticProvider BidirectionalRelation = - DiagnosticProvider.Create(static (model, diagnostics) => - { - var bidirectionalRelationNames = model.Relations - .Where(r => r.RelatedType.Signature.Names.FullGenericName == model.Signature.Names.FullGenericName) - .Select(r => r.RelatedType.Signature.Names.FullGenericName); - - if(!bidirectionalRelationNames.Any()) - return; - - Add(model, diagnostics, - l => bidirectionalRelationNames.Select(n => Diagnostics.BidirectionalRelation(l, n))); - }); - public static readonly IDiagnosticProvider DuplicateRelation = - DiagnosticProvider.Create(static (model, diagnostics) => - { - var duplicateRelationNames = model.Relations - .GroupBy(r => r.RelatedType.Signature.Names.FullGenericName) - .Select(g => g.ToArray()) - .Where(g => g.Length > 1) - .Select(g => g[0].RelatedType.Signature.Names.FullGenericName); - - if(!duplicateRelationNames.Any()) - return; - - Add(model, diagnostics, - l => duplicateRelationNames.Select(n => Diagnostics.DuplicateRelation(l, n))); - }); - public static IDiagnosticProvider GenericRelation = - DiagnosticProvider.Create(static (model, diagnostics) => - { - var relations = model.Relations - .Where(r => r.RelatedType.Signature.IsGenericType); - - if(!( model.Signature.IsGenericType && relations.Any() )) - return; - - Add(model, diagnostics, - Diagnostics.GenericRelation); - }); - public static IDiagnosticProvider StorageSelectionViolations = - DiagnosticProvider.Create(static (model, diagnostics) => - { - var offendingRepresentables = model.RepresentableTypes - .Where(d => d.StorageStrategy.Value.Violation != StorageSelectionViolation.None); - - foreach(var representableType in offendingRepresentables) - { - var name = representableType.Signature.Names.FullGenericNullableName; - var violation = representableType.StorageStrategy.Value.Violation; - - var factory = violation switch - { - StorageSelectionViolation.PureValueReferenceSelection => - //cast once, infer following exprs - (Func)Diagnostics.BoxingStrategy, - StorageSelectionViolation.PureValueValueSelectionGeneric => - Diagnostics.GenericViolationStrategy, - StorageSelectionViolation.ImpureValueReference => - Diagnostics.BoxingStrategy, - StorageSelectionViolation.ImpureValueValue => - Diagnostics.TleStrategy, - StorageSelectionViolation.ReferenceValue => - Diagnostics.TleStrategy, - StorageSelectionViolation.UnknownReference => - Diagnostics.PossibleBoxingStrategy, - StorageSelectionViolation.UnknownValue => - Diagnostics.PossibleTleStrategy, - _ => null - }; - - if(factory != null) - { - Add(model, diagnostics, - l => factory.Invoke(l, name)); - } - } - }); - public static IDiagnosticProvider SmallGenericUnion = - DiagnosticProvider.Create(static (model, diagnostics) => - { - if(!model.Signature.IsGenericType || model.Settings.Layout != LayoutSetting.Small) - { - return; - } - - var newLocations = model.Locations.Value - .Select(Diagnostics.SmallGenericUnion); - diagnostics.AddRange(newLocations); - }); - //public static IDiagnosticProvider UnknownGenericParameterName = - // DiagnosticProvider.Create(static (model, diagnostics) => - // { - // var available = model.Symbol.TypeParameters - // .Select(p => p.Name) - // .ToImmutableHashSet(); - // var unknowns = model.Annotations.AllRepresentableTypes - // .Where(a => a.Attribute.RepresentableTypeIsGenericParameter) - // .Where(a => !available.Contains(a.Names.SimpleTypeName)) - // .Select(a => a.Names.SimpleTypeName) - // .ToArray(); - - // if(unknowns.Length == 0) - // return; - - // var location = model.TargetDeclaration.GetLocation(); - - // foreach(var unknown in unknowns) - // { - // var diagnostic = Diagnostics.UnknownGenericParameterName(location, unknown); - // _ = diagnostics.Add(diagnostic); - // } - // }); - public static IDiagnosticProvider ReservedGenericParameterName = - DiagnosticProvider.Create(static (model, diagnostics) => - { - var collisions = model.Signature.TypeArgs - .Select(p => p.Names.Name) - .Where(model.Settings.IsReservedGenericTypeName); - - if(!collisions.Any()) - return; - - var newDiagnostics = model.Locations.Value - .SelectMany(l => collisions - .Select(c => Diagnostics.ReservedGenericParameterName(l, c))); - diagnostics.AddRange(newDiagnostics); - }); - public static IDiagnosticProvider OperatorOmissions = - DiagnosticProvider.Create(static (model, diagnostics) => - { - var omissions = model.RepresentableTypes - .Where(t => t.OmitConversionOperators); - - var interfaceOmissions = omissions.Where(o => o.Signature.IsInterface); - - foreach(var interfaceOmission in interfaceOmissions) - { - Add(model, diagnostics, - l => Diagnostics.RepresentableTypeIsInterface( - l, - interfaceOmission.Signature.Names.FullGenericNullableName)); - } - - var supertypeOmissions = omissions.Where(o => o.IsBaseClassToUnionType); - - foreach(var supertype in supertypeOmissions) - { - Add(model, diagnostics, - l => Diagnostics.RepresentableTypeIsSupertype( - l, - supertype.Signature.Names.FullGenericNullableName)); - } - }); - //public static IDiagnosticProvider UnionTypeSettingsOnNonUnionType = - // DiagnosticProvider.Create(static (model, diagnostics) => - // { - // TODO - // var representableTypes = model.RepresentableTypes; - - // if(representableTypes.Count > 0 || /*settings not located ?*/) - // { - // return; - // } - - // var location = model.TargetDeclaration.GetLocation(); - // var diagnostic = Diagnostics.UnionTypeSettingsOnNonUnionType(location); - // _ = diagnostics.Add(diagnostic); - // }); - public static IDiagnosticProvider ImplicitConversionIfSolitary = - DiagnosticProvider.Create(static (model, diagnostics) => - { - if(model.RepresentableTypes.Count > 1 && - model.RepresentableTypes.Any(a => a.Options.HasFlag(UnionTypeOptions.ImplicitConversionIfSolitary))) - { - Add(model, diagnostics, - Diagnostics.ImplicitConversionOptionOnNonSolitary); - } - }); - public static IDiagnosticProvider UnionTypeCount = - DiagnosticProvider.Create(static (model, diagnostics) => - { - var count = model.RepresentableTypes.Count; - if(count <= Qualifications.MaxRepresentableTypesCount) - return; - - Add(model, diagnostics, - Diagnostics.TooManyTypes); - }); - //public static IDiagnosticProvider Partiality = - // DiagnosticProvider.Create(static (model, diagnostics) => - // { - // if(model.TargetDeclaration.IsPartial()) - // return; - - // var location = model.TargetDeclaration.Identifier.GetLocation(); - // var diagnostic = Diagnostics.NonPartialDeclaration(location); - // _ = diagnostics.Add(diagnostic); - // }); - public static IDiagnosticProvider NonStatic = - DiagnosticProvider.Create(static (model, diagnostics) => - { - if(!model.Signature.IsStatic) - return; - - Add(model, diagnostics, - Diagnostics.StaticTarget); - }); - public static IDiagnosticProvider NonRecord = - DiagnosticProvider.Create(static (model, diagnostics) => - { - if(!model.Signature.IsRecord) - return; - - Add(model, diagnostics, - Diagnostics.RecordTarget); - }); - /* - public static IDiagnosticProvider UnionTypeAttribute = - DiagnosticProvider.Create(static (model, diagnostics) => - { - if(model.Annotations.AllRepresentableTypes.Count > 0) - return; - - var location = model.TargetDeclaration.Identifier.GetLocation(); - var diagnostic = Diagnostics.MissingUnionTypeAttribute(location); - _ = diagnostics.Add(diagnostic); - }); - */ - public static IDiagnosticProvider UniqueUnionTypeAttributes = - DiagnosticProvider.Create(static (model, diagnostics) => - { - var duplicateRepresentableTypeNames = model.RepresentableTypes - .Select(t => t.Signature.Names.FullGenericName) - .GroupBy(n => n) - .Where(g => g.Skip(1).Any()) - .Select(g => g.First()); - - if(!duplicateRepresentableTypeNames.Any()) - return; - - Add(model, diagnostics, - l => duplicateRepresentableTypeNames.Select(t => - Diagnostics.DuplicateUnionTypeAttributes(l, t))); - }); - public static IDiagnosticProvider AliasCollisions = - DiagnosticProvider.Create(static (model, diagnostics) => - { - - var duplicateRepresentableTypeAliae = model.RepresentableTypes - .Select(t => t.Alias) - .GroupBy(n => n) - .Where(g => g.Skip(1).Any()) - .Select(g => g.First()); - - if(!duplicateRepresentableTypeAliae.Any()) - return; - - Add(model, diagnostics, - l => duplicateRepresentableTypeAliae.Select(t => - Diagnostics.AliasCollision(l, t))); - }); - public static IDiagnosticProvider SelfReference = - DiagnosticProvider.Create(static (model, diagnostics) => - { - var hasSelfReference = model.RepresentableTypes - .Any(t => t.Signature.Equals(model.Signature)); - - if(!hasSelfReference) - return; - - Add(model, diagnostics, - Diagnostics.SelfReference); - }); - public static IDiagnosticProvider Inheritance = - DiagnosticProvider.Create(static (model, diagnostics) => - { - if(model.Signature.HasNoBaseClass) - return; - - Add(model, diagnostics, - Diagnostics.Inheritance); - }); - - public static IEnumerable> All = new[] - { - BidirectionalRelation, - DuplicateRelation, - GenericRelation, - StorageSelectionViolations, - SmallGenericUnion, - //UnknownGenericParameterName, - ReservedGenericParameterName, - OperatorOmissions, - //UnionTypeSettingsOnNonUnionType, - ImplicitConversionIfSolitary, - UnionTypeCount, - //Partiality, - NonStatic, - NonRecord, - //UnionTypeAttribute, - UniqueUnionTypeAttributes, - AliasCollisions, - SelfReference, - Inheritance, - NullableOptionOnValueType - }; -} diff --git a/UnionsGenerator/Attributes/RelationAttribute.cs b/UnionsGenerator/Attributes/RelationAttribute.cs deleted file mode 100644 index 90502de..0000000 --- a/UnionsGenerator/Attributes/RelationAttribute.cs +++ /dev/null @@ -1,101 +0,0 @@ -// -// This file was generated by RhoMicro.CodeAnalysis.UnionsGenerator -// The tool used to generate this code may be subject to license terms; -// this generated code is however not subject to those terms, instead it is -// subject to the license (if any) applied to the containing project. -// -#nullable enable -#pragma warning disable - -namespace RhoMicro.CodeAnalysis -{ - using System; - - /// - /// Marks the target type to be related to another union type. - /// - /// The type to register as related to the target union type. - [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] -#if UNIONS_GENERATOR - [GenerateFactory(OmitTypeCheck = true)] -#endif - sealed partial class RelationAttribute : Attribute - { } - /// - /// Marks the target type to be related to other union types. - /// - /// The first type to register as related to the target union type. - /// The second type to register as related to the target union type. - [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - sealed partial class RelationAttribute : Attribute - { } - /// - /// Marks the target type to be related to other union types. - /// - /// The first type to register as related to the target union type. - /// The second type to register as related to the target union type. - /// The third type to register as related to the target union type. - [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - sealed partial class RelationAttribute : Attribute - { } - /// - /// Marks the target type to be related to other union types. - /// - /// The first type to register as related to the target union type. - /// The second type to register as related to the target union type. - /// The third type to register as related to the target union type. - /// The fourth type to register as related to the target union type. - [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - sealed partial class RelationAttribute : Attribute - { } - /// - /// Marks the target type to be related to other union types. - /// - /// The first type to register as related to the target union type. - /// The second type to register as related to the target union type. - /// The third type to register as related to the target union type. - /// The fourth type to register as related to the target union type. - /// The fifth type to register as related to the target union type. - [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - sealed partial class RelationAttribute : Attribute - { } - /// - /// Marks the target type to be related to other union types. - /// - /// The first type to register as related to the target union type. - /// The second type to register as related to the target union type. - /// The third type to register as related to the target union type. - /// The fourth type to register as related to the target union type. - /// The fifth type to register as related to the target union type. - /// The sixth type to register as related to the target union type. - [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - sealed partial class RelationAttribute : Attribute - { } - /// - /// Marks the target type to be related to other union types. - /// - /// The first type to register as related to the target union type. - /// The second type to register as related to the target union type. - /// The third type to register as related to the target union type. - /// The fourth type to register as related to the target union type. - /// The fifth type to register as related to the target union type. - /// The sixth type to register as related to the target union type. - /// The seventh type to register as related to the target union type. - [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - sealed partial class RelationAttribute : Attribute - { } - /// - /// Marks the target type to be related to other union types. - /// - /// The first type to register as related to the target union type. - /// The second type to register as related to the target union type. - /// The third type to register as related to the target union type. - /// The fourth type to register as related to the target union type. - /// The fifth type to register as related to the target union type. - /// The sixth type to register as related to the target union type. - /// The seventh type to register as related to the target union type. - /// The eighth type to register as related to the target union type. - [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - sealed partial class RelationAttribute : Attribute - { } -} diff --git a/UnionsGenerator/Attributes/UnionTypeAttribute.Internal.cs b/UnionsGenerator/Attributes/UnionTypeAttribute.Internal.cs deleted file mode 100644 index 0976624..0000000 --- a/UnionsGenerator/Attributes/UnionTypeAttribute.Internal.cs +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis; - -using Microsoft.CodeAnalysis; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -internal partial class AliasedUnionTypeBaseAttribute -{ - internal PartialRepresentableTypeModel GetPartialModel(TypeOrTypeParameterType representableType, INamedTypeSymbol unionType, CancellationToken ct) - { - var result = PartialRepresentableTypeModel.Create( - Alias, - FactoryName, - Options, - Storage, - new(Groups), - representableType, - unionType, - ct); - - return result; - } -} diff --git a/UnionsGenerator/Attributes/UnionTypeAttribute.cs b/UnionsGenerator/Attributes/UnionTypeAttribute.cs deleted file mode 100644 index 37afb08..0000000 --- a/UnionsGenerator/Attributes/UnionTypeAttribute.cs +++ /dev/null @@ -1,292 +0,0 @@ -// -// This file was generated by RhoMicro.CodeAnalysis.UnionsGenerator -// The tool used to generate this code may be subject to license terms; -// this generated code is however not subject to those terms, instead it is -// subject to the license (if any) applied to the containing project. -// -#nullable enable -#pragma warning disable - -namespace RhoMicro.CodeAnalysis -{ - using System; - using System.Collections.Generic; - - /// - /// Defines options for generating union types. - /// - [Flags] - enum UnionTypeOptions - { - /// - /// The default options. - /// - Default = ImplicitConversionIfSolitary, - /// - /// - None = 0x00, - /// - /// Instructs the generator to emit an implicit conversion to the representable type if it is the only one. - /// In effect, this option will enable the union type to act as an alias wrapper for the representable type. - /// - ImplicitConversionIfSolitary = 0x01, - /// - /// Instructs the generator to emit a superset conversion operator implementation even though - /// the representable type is a generic type parameter. By default, it is omitted because of possible - /// unification for certain generic arguments. - /// - //SupersetOfParameter = 0x02, - /// - /// Instructs the generator to treat the representable reference type - /// as nullable, allowing for - /// arguments in factories, conversions etc. - /// If you are trying to use a nullable value type, use the ? annotation or - /// , where T is the value type. - /// - Nullable = 0x04 - } - - /// - /// Defines options for the storage implementation of a representable type. - /// In order for the generator to generate an efficient storage implementation, - /// consumers should communicate whether the representable type is known to - /// be a struct, class or of unknown nature. This is mostly relevant for generic - /// type parameters, however an explicit strategy may be selected for any representable - /// type. Whether or not generic type parameters are known to be reference - /// or value types depends on their constraints. Parameters constrained to - /// will be assumed to be value types. Conversely, - /// parameters constrained to will be assumed to be reference types. - /// - /* - | box |value| auto | field - struct | rc! | vc | vc | cc - class | rc | rc! | rc | cc - none | rc! | vc! | rc! | cc - */ - enum StorageOption - { - /// - /// The generator will automatically decide on a storage strategy. - /// - /// If the representable type is known to be a value type, - /// this will store values of that type inside a shared value type container. - /// Boxing will not occur. - /// - /// - /// If the representable type is known to be a reference type, - /// this will store values of that type inside a shared reference type container. - /// - /// - /// If the representable type is neither known to be a reference type - /// nor a value type, this option will cause values of that type to - /// be stored inside a shared reference type container. - /// If the representable type is a generic type parameter, - /// boxing will occur for value type arguments to that parameter. - /// - /// - Auto, - - /// - /// The generator will always store values of the representable type - /// inside a shared reference type container. - /// - /// If the representable type is known to be a value type, - /// boxing will occur. - /// - /// - /// If the representable type is a generic type parameter, - /// boxing will occur for value type arguments to that parameter. - /// - /// - Reference, - - /// - /// The generator will attempt to store values of the representable type - /// inside a value type container. - /// - /// If the representable type is known to be a value type, - /// this will store values of that type inside a shared value type container. - /// Boxing will not occur. - /// - /// - /// If the representable type is known to be a reference type, - /// this will store values of that type inside a shared reference type container. - /// Boxing will not occur. - /// - /// - /// If the representable type is neither known to be a reference type - /// nor a value type, this option will cause values of that type to - /// be stored inside a shared value type container. - /// If the representable type is a generic type parameter, - /// an exception of type will occur for - /// reference type arguments to that parameter. - /// - /// - Value, - - /// - /// The generator will attempt to store values of the representable type - /// inside a dedicated container for that type. - /// - /// If the representable type is known to be a value type, - /// this will store values of that type inside a dedicated - /// value type container. - /// Boxing will not occur. - /// - /// - /// If the representable type is known to be a reference type, - /// this will store values of that type inside a - /// dedicated reference type container. - /// - /// - /// If the representable type is neither known to be a reference type - /// nor a value type, this option will cause values of that type to - /// be stored inside a dedicated strongly typed container. - /// Boxing will not occur. - /// - /// - Field - } - - /// - /// Marks the target type as a union type being able to represent the type passed to the constructor. - /// - [AttributeUsage(( (AttributeTargets)( -1 ) ))] - partial class UnionTypeBaseAttribute : Attribute - { - /// - /// Gets or sets the alias groups that the representable type is to be a part of. - /// Represnetable types that share a group may be checked for using unified methods - /// and properties like IsGroup where Group is the name of the group - /// that the representable type is a part of. - /// - public virtual String[] Groups { get; set; } = Array.Empty(); - - /// - /// Gets or sets the generator options to use. - /// - public virtual UnionTypeOptions Options { get; set; } = UnionTypeOptions.Default; - - /// - /// Gets or sets the option defining storage generation. - /// - public virtual StorageOption Storage { get; set; } - } - - [AttributeUsage(( (AttributeTargets)( -1 ) ))] -#if UNIONS_GENERATOR - [GenerateFactory(OmitTypeCheck = true)] -#endif - partial class AliasedUnionTypeBaseAttribute : UnionTypeBaseAttribute - { - /// - /// Gets or sets the alias to use for members representing the type represented by the union. - /// For example, the represented type would be represented using names like - /// list_of_T. Setting this property to yourAlias will instruct the generator to use - /// member names like yourAlias instead of list_of_T. Use this property to avoid - /// name collisions in generated code. Since the alias will be used for member names, it will - /// only be taken into account if it is a valid identifier name. - /// - public String? Alias { get; set; } - /// - /// Gets or sets the name of the generated static factory method used to create instances - /// of the union type with the representable type. - /// - public String FactoryName { get; set; } = "Create"; - /// - public override String[] Groups { get => base.Groups; set => base.Groups = value; } - /// - public override UnionTypeOptions Options { get => base.Options; set => base.Options = value; } - /// - public override StorageOption Storage { get => base.Storage; set => base.Storage = value; } - } - /// - /// Marks the target type as a union type being able to represent . - /// - [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - sealed partial class UnionTypeAttribute : AliasedUnionTypeBaseAttribute - { } - /// - /// Marks the target type as a union type being able to represent - /// - /// and . - /// - [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - sealed partial class UnionTypeAttribute : UnionTypeBaseAttribute - { } - /// - /// Marks the target type as a union type being able to represent - /// , - /// - /// and . - /// - [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - sealed partial class UnionTypeAttribute : UnionTypeBaseAttribute - { } - /// - /// Marks the target type as a union type being able to represent - /// , - /// , - /// - /// and . - /// - [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - sealed partial class UnionTypeAttribute : UnionTypeBaseAttribute - { } - /// - /// Marks the target type as a union type being able to represent - /// , - /// , - /// , - /// - /// and . - /// - [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - sealed partial class UnionTypeAttribute : UnionTypeBaseAttribute - { } - /// - /// Marks the target type as a union type being able to represent - /// , - /// , - /// , - /// , - /// - /// and . - /// - [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - sealed partial class UnionTypeAttribute : UnionTypeBaseAttribute - { } - /// - /// Marks the target type as a union type being able to represent - /// , - /// , - /// , - /// , - /// , - /// - /// and . - /// - [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - sealed partial class UnionTypeAttribute : UnionTypeBaseAttribute - { } - /// - /// Marks the target type as a union type being able to represent - /// , - /// , - /// , - /// , - /// , - /// , - /// - /// and . - /// - [AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, AllowMultiple = true, Inherited = false)] - sealed partial class UnionTypeAttribute : UnionTypeBaseAttribute - { } - /// - /// Marks the target type as a union type being able to represent the annotated type parameter. - /// - [AttributeUsage(AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] - sealed partial class UnionTypeAttribute : AliasedUnionTypeBaseAttribute - { } -} \ No newline at end of file diff --git a/UnionsGenerator/Attributes/UnionTypeFactoryAttribute.cs b/UnionsGenerator/Attributes/UnionTypeFactoryAttribute.cs deleted file mode 100644 index 4ed8907..0000000 --- a/UnionsGenerator/Attributes/UnionTypeFactoryAttribute.cs +++ /dev/null @@ -1,27 +0,0 @@ -// -// This file was generated by RhoMicro.CodeAnalysis.UnionsGenerator -// The tool used to generate this code may be subject to license terms; -// this generated code is however not subject to those terms, instead it is -// subject to the license (if any) applied to the containing project. -// -#nullable enable -#pragma warning disable - -namespace RhoMicro.CodeAnalysis -{ - using System; - - /// - /// Marks the target method as the factory method to use when instantiating - /// an instance of the union type representing a value of the annotated parameter. - /// Factory methods must be static, have no type parameters and only have one - /// parameter of a type representable by the union type. - /// Factory polymorphism is not yet supported. - /// - [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] -#if UNIONS_GENERATOR - [GenerateFactory] -#endif - sealed partial class UnionTypeFactoryAttribute : Attribute - { } -} diff --git a/UnionsGenerator/Attributes/UnionTypeSettingsAttribute.cs b/UnionsGenerator/Attributes/UnionTypeSettingsAttribute.cs deleted file mode 100644 index 6d88fa0..0000000 --- a/UnionsGenerator/Attributes/UnionTypeSettingsAttribute.cs +++ /dev/null @@ -1,276 +0,0 @@ -// -// This file was generated by RhoMicro.CodeAnalysis.UnionsGenerator -// The tool used to generate this code may be subject to license terms; -// this generated code is however not subject to those terms, instead it is -// subject to the license (if any) applied to the containing project. -// -#nullable enable -#pragma warning disable - -namespace RhoMicro.CodeAnalysis; - -using System; - -#region Setting Enums -/// -/// Defines settings for generating an implementation of . -/// -enum ToStringSetting -{ - /// - /// The generator will emit an implementation that returns detailed information, including: - /// - /// the name of the union type - /// a list of types representable by the union type - /// an indication of which type 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 for annotating the target with an instance of . -/// -enum LayoutSetting -{ - /// - /// Generate an annotation optimized for size. - /// - Small, - /// - /// Do not generate any annotations. - /// - Auto -} -/// -/// Defines settings for controlling the accessibility of generated constructors. -/// -enum ConstructorAccessibilitySetting -{ - /// - /// Generated constructors should always be private, unless - /// no conversion operators are generated for the type they - /// accept. This would be the case for interface types or - /// supertypes of the target union. - /// - PublicIfInconvertible, - /// - /// Generated constructors should always be private. - /// - Private, - /// - /// Generated constructors should always be public - /// - Public -} -/// -/// Defines settings on how to implement interfaces that all representable -/// types implement. -/// -enum InterfaceMatchSetting -{ - /// - /// Generated interface implementations should be explicit if at least - /// one of the representable types implements the interface explicitly; - /// otherwise, interface implementations should be implicit. - /// - Auto, - /// - /// Generated interface implementations should always be explicit. - /// - Explicit, - /// - /// Generated interface implementations should always be implicit. - /// - Implicit, - /// - /// No interfaces implementations should be generated. - /// - Omit -} -/// -/// Defines settings for the kind of diagnostics to report. -/// -[Flags] -enum DiagnosticsLevelSettings -{ - /// - /// Instructs the analyzer not to emit diagnostics - /// - None = 0x00, - /// - /// Instructs the analyzer to report info diagnostics. - /// - Info = 0x01, - /// - /// Instructs the analyzer to report warning diagnostics. - /// - Warning = 0x02, - /// - /// Instructs the analyzer to report error diagnostics. - /// - Error = 0x04, - /// - /// Instructs the analyzer to report all diagnostics. - /// - All = Info | Warning | Error -} -/// -/// Defines miscellaneous settings. -/// -[Flags] -enum MiscellaneousSettings -{ - /// - /// - None = 0x00, - /// - /// The default settings. - /// - Default = None, - /// - /// Indicates whether the generated source code should be available as a string constant on the union type itself. - /// This setting is generally only useful if the generated implementation should be emitted from another generator. - /// - EmitGeneratedSourceCode = 0x01, - /// - /// Indicates whether to generate a custom converter type - /// for System.Text.Json deserialization. If set, this will also cause - /// the union type to be annotated with an appropriate JsonConverter attribute. - /// - GenerateJsonConverter = 0x02, - /// - /// Indicates that the generator should emit a comment detailing the structure of the union type. - /// - EmitStructuralRepresentation = 0x04 -} - -/// -/// Defines settings pertaining to equality operator implementations. -/// -enum EqualityOperatorsSetting -{ - /// - /// 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 -} - -#endregion -#region Attribute Declaration -/// -/// 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. -/// -[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class | AttributeTargets.Assembly, AllowMultiple = false, Inherited = false)] -#if UNIONS_GENERATOR -[GenerateFactory] -#endif -sealed partial class UnionTypeSettingsAttribute : Attribute -{ - #region Settings - /// - /// Defines how to generate an implementation . - /// - public ToStringSetting ToStringSetting { get; set; } = ToStringSetting.Detailed; - /// - /// Defines whether to generate a size optimizing annotation. - /// - public LayoutSetting Layout { get; set; } = LayoutSetting.Auto; - /// - /// The level of diagnostics to be reported by the analyzer. - /// - public DiagnosticsLevelSettings DiagnosticsLevel { get; set; } = DiagnosticsLevelSettings.All; - /// - /// The desired accessibility of generated constructors. - /// - public ConstructorAccessibilitySetting ConstructorAccessibility { get; set; } = ConstructorAccessibilitySetting.Private; - /// - /// Indicates how to generate implementations for - /// interfaces implemented by all representable types. Implementations will - /// map calls to interface instance methods and properties onto the represented - /// value. - /// - /// Please note that currently, only fully bound and constructed interface implementations are supported. - /// - /// - public InterfaceMatchSetting InterfaceMatchSetting { get; set; } = InterfaceMatchSetting.Auto; - /// - /// Indicates how to generate equality operators. - /// By default, equality operators will only be emitted for value types, so as to preserve - /// reference equality for comparing reference union types via == or !=. - /// - public EqualityOperatorsSetting EqualityOperatorsSetting { get; set; } = EqualityOperatorsSetting.EmitOperatorsIfValueType; - /// - /// Gets or sets miscellaneous settings. - /// - public MiscellaneousSettings Miscellaneous { get; set; } = MiscellaneousSettings.Default; - #endregion - #region Strings - /// - /// A raw code preface to prepend before the generated type declaration. - /// - public String TypeDeclarationPreface { get; set; } = ""; - /// - /// The name of the generic parameter for generic Is, As and factory methods. - /// Set this property in order to avoid name collisions with generic union type parameters - /// - public String GenericTValueName { get; set; } = "TValue"; - /// - /// The name of the generic parameter for the TryConvert method. - /// Set this property in order to avoid name collisions with generic union type parameters - /// - public String TryConvertTypeName { get; set; } = "TUnion"; - /// - /// The name of the generic parameter for the Match method. - /// Set this property in order to avoid name collisions with generic union type parameters - /// - public String MatchTypeName { get; set; } = "TMatchResult"; - /// - /// The name to use for the discriminating tag type. - /// - public String TagTypeName { get; set; } = "__Tag"; - /// - /// The name to use for the container type containing value types. - /// - public String ValueTypeContainerTypeName { get; set; } = "__ValueTypeContainer"; - /// - /// The name to use for the field containing value types. - /// - public String ValueTypeContainerName { get; set; } = "__value"; - /// - /// The name to use for the field containing reference types. - /// - public String ReferenceTypeContainerName { get; set; } = "__reference"; - /// - /// The name to use for the field containing the discriminating tag. - /// - public String TagFieldName { get; set; } = "__tag"; - /// - /// The name to use for the default (uninitialized) tag value. - /// - public String TagNoneName { get; set; } = "__None"; - /// - /// The name of the generated json converter type. - /// - public String JsonConverterTypeName { get; set; } = "JsonConverter"; - #endregion -} -#endregion \ No newline at end of file diff --git a/UnionsGenerator/Generators/SettingsAttributeData.cs b/UnionsGenerator/Generators/SettingsAttributeData.cs deleted file mode 100644 index 02b5411..0000000 --- a/UnionsGenerator/Generators/SettingsAttributeData.cs +++ /dev/null @@ -1,149 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Generators; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.CSharp; - -using RhoMicro.CodeAnalysis; -using RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -using System; -using System.Collections.Immutable; -using System.Reflection; - -internal sealed class SettingsAttributeData : AttributeData, IEquatable -{ - #region Constructors - private SettingsAttributeData( - AttributeData declaredSettings, - ImmutableArray> namedArguments, - Dictionary namedArgumentsMap) - { - _namedArgumentsMap = namedArgumentsMap; - - CommonAttributeClass = declaredSettings.AttributeClass; - CommonAttributeConstructor = declaredSettings.AttributeConstructor; - CommonApplicationSyntaxReference = declaredSettings.ApplicationSyntaxReference; - CommonConstructorArguments = declaredSettings.ConstructorArguments; - CommonNamedArguments = namedArguments; - } - private SettingsAttributeData( - AttributeData declaredSettings, - Dictionary namedArgumentsMap) - : this(declaredSettings, declaredSettings.NamedArguments, namedArgumentsMap) - { } - private SettingsAttributeData() - { - _namedArgumentsMap = []; - CommonConstructorArguments = ImmutableArray.Create(); - CommonNamedArguments = ImmutableArray.Create>(); - } - #endregion - - public static SettingsAttributeData Default { get; } = new(); - - private readonly Dictionary _namedArgumentsMap; - - protected override INamedTypeSymbol? CommonAttributeClass { get; } - protected override IMethodSymbol? CommonAttributeConstructor { get; } - protected override SyntaxReference? CommonApplicationSyntaxReference { get; } - protected override ImmutableArray CommonConstructorArguments { get; } - protected override ImmutableArray> CommonNamedArguments { get; } - - public static SettingsAttributeData CreateAssemblySettingsWithDefaults(AttributeData assemblySettings) - => CreateWithDefaults(assemblySettings); - public static SettingsAttributeData CreateDeclaredSettings(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var declaredSettings = context.Attributes[0]; - - var declaredPropsMap = MapProperties(declaredSettings); - - var result = new SettingsAttributeData(declaredSettings, declaredPropsMap); - - return result; - } - public static SettingsAttributeData CreateDeclaredSettingsWithDefaults( - AttributeData declaredSettings, - SettingsAttributeData assemblySettings) - => CreateWithDefaults(declaredSettings, assemblySettings); - - private static SettingsAttributeData CreateWithDefaults( - AttributeData declaredSettings, - SettingsAttributeData? fallbackSettings = null) - { - var declaredPropsMap = MapProperties(declaredSettings); - var assemblyPropsMap = fallbackSettings?._namedArgumentsMap ?? []; - var namedArgsMap = SettingsModel.PropertyNames - .Select(n => - declaredPropsMap.TryGetValue(n, out var v) ? (n, v) : - assemblyPropsMap.TryGetValue(n, out v) ? (n, v) : null) - .Where(c => c.HasValue) - .ToDictionary(t => t!.Value.name, t => t!.Value.value); - var namedArgs = namedArgsMap.ToImmutableArray(); - var result = new SettingsAttributeData(declaredSettings, namedArgs, namedArgsMap); - - return result; - } - - private static Dictionary MapProperties(AttributeData data) => - data.NamedArguments - .Where(kvp => kvp.Key != null) - .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - - public override Boolean Equals(Object? obj) => Equals(obj as SettingsAttributeData); - public Boolean Equals(SettingsAttributeData? other) - { - if(_namedArgumentsMap.Count != other?._namedArgumentsMap.Count) - return false; - - foreach(var (name, constant) in _namedArgumentsMap) - { - if(!other._namedArgumentsMap.TryGetValue(name, out var otherConstant) || - !Equals(constant, otherConstant)) - { - return false; - } - } - - return true; - } - - private static Boolean Equals(TypedConstant a, TypedConstant b) - { - if(a.Kind != b.Kind || a.IsNull != b.IsNull) - return false; - - switch(a.Kind) - { - case TypedConstantKind.Primitive or TypedConstantKind.Enum - when !EqualityComparer.Default.Equals(a.Value, b.Value): - return false; - case TypedConstantKind.Type - when !SymbolEqualityComparer.IncludeNullability.Equals(a.Type, b.Type): - return false; - case TypedConstantKind.Array: - { - if(a.Values.Length != b.Values.Length) - return false; - - for(var i = 0; i < a.Values.Length; i++) - { - var aValue = a.Values[i]; - var bValue = b.Values[i]; - if(!Equals(aValue, bValue)) - return false; - } - - break; - } - } - - return true; - } - - public override Int32 GetHashCode() => throw new NotSupportedException("GetHashCode is not supported on settings attribute data."); -} diff --git a/UnionsGenerator/Generators/UnionsGenerator.cs b/UnionsGenerator/Generators/UnionsGenerator.cs deleted file mode 100644 index 21c1cf7..0000000 --- a/UnionsGenerator/Generators/UnionsGenerator.cs +++ /dev/null @@ -1,525 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Generators; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -using RhoMicro.CodeAnalysis; -using RhoMicro.CodeAnalysis.Library; -using RhoMicro.CodeAnalysis.Library.Text; -using RhoMicro.CodeAnalysis.UnionsGenerator.Models; -using RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Visitors; -using RhoMicro.CodeAnalysis.UnionsGenerator.Utils; - -using System; - -using FactoryMapProvider = Microsoft.CodeAnalysis.IncrementalValueProvider>>; -using PartialUnionTypeModelsProvider = Microsoft.CodeAnalysis.IncrementalValueProvider>; -using PartialTargetTypeModelUnionProvider = Microsoft.CodeAnalysis.IncrementalValueProvider representableTypes, Boolean isEqualsRequired, Boolean doesNotImplementToString)>>; -using RelationsProvider = Microsoft.CodeAnalysis.IncrementalValueProvider>>; -using SettingsMapProvider = Microsoft.CodeAnalysis.IncrementalValueProvider<(Models.SettingsModel fallbackSettings, EquatableDictionary definedSettings)>; -using SourceTextProvider = Microsoft.CodeAnalysis.IncrementalValuesProvider<(String hintName, String source)>; -using System.Linq; -using System.Collections.Immutable; -using RhoMicro.CodeAnalysis.UnionsGenerator.Analyzers; -using Microsoft.CodeAnalysis.CSharp; - -/// -/// Generates partial union type implementations. -/// -[Generator(LanguageNames.CSharp)] -public class UnionsGenerator : IIncrementalGenerator -{ - /// - /// Gets constant source texts to be generated into target projects. - /// - public static IReadOnlyList ConstantSourceTexts { get; } = - [ - AliasedUnionTypeBaseAttribute.SourceText, - RelationAttribute.SourceText, - UnionTypeFactoryAttribute.SourceText, - UnionTypeSettingsAttribute.SourceText, - ConstantSources.Util - ]; - /// - public void Initialize(IncrementalGeneratorInitializationContext context) - { - var factoryMapProvider = CreateFactoryMapProvider(context); - var settingsProvider = CreateSettingsProvider(context); - var relationsProvider = CreateRelationsProvider(context); - - var typeTargetProvider = CreateUnionTypeProvider(context); - var partialTargetProvider = UnionPartialTypeModelProviders(typeTargetProvider); - - var targetTypeModelsProvider = UnifyPartialModels(partialTargetProvider, settingsProvider, factoryMapProvider, relationsProvider); - var sourceTextProvider = CreateSourceTextProvider(targetTypeModelsProvider); - - context.RegisterSourceOutput(sourceTextProvider, (ctx, sourceData) => - { - ctx.CancellationToken.ThrowIfCancellationRequested(); - ctx.AddSource(sourceData.hintName, sourceData.source); - }); - context.RegisterPostInitializationOutput(RegisterConstantSources); - } - #region Transformations - private static SourceTextProvider CreateSourceTextProvider(IncrementalValuesProvider provider) => - provider - .Select((model, ct) => - { - var containsErrors = DiagnosticsAccumulator.Create(model) - .DiagnoseNonHiddenSeverities() - .Receive(Providers.All, ct) - .ContainsErrors; - - return (model, containsErrors); - }) - .Where(t => !t.containsErrors) - .Select((tuple, ct) => - { - ct.ThrowIfCancellationRequested(); - - var (model, _) = tuple; - - var resultBuilder = new IndentedStringBuilder( - IndentedStringBuilderOptions.GeneratedFile with - { - AmbientCancellationToken = ct, - GeneratorName = "RhoMicro.CodeAnalysis.UnionsGenerator", - PrependWarningDisablePragma = true - }); - - _ = resultBuilder.OpenRegionBlock($"Implementation of {model.Signature.Names.FullGenericName}"); - - if(model.Settings.Miscellaneous.HasFlag(MiscellaneousSettings.EmitStructuralRepresentation)) - { - _ = resultBuilder.OpenRegionBlock("Structural Representation").OpenBlock(new(Indentation: "// ")); - - new StructuralRepresentationVisitor(resultBuilder).Visit(model); - - resultBuilder.CloseBlock().CloseBlockCore(); - } - - new SourceTextVisitor(resultBuilder).Visit(model); - - var source = resultBuilder.CloseBlock().ToString(); - var hint = model.Signature.Names.FullIdentifierOrHintName; - var hintName = $"{hint}.g.cs"; - - return (hintName, source); - }); - #endregion - #region Combinations - private static IncrementalValuesProvider UnifyPartialModels( - PartialTargetTypeModelUnionProvider partialsProvider, - SettingsMapProvider settingsProvider, - FactoryMapProvider factoriesProvider, - RelationsProvider relationsProvider) => - partialsProvider - .Combine(settingsProvider) - .Combine(factoriesProvider) - .Combine(relationsProvider) - .Select((tuple, ct) => - { - ct.ThrowIfCancellationRequested(); - - var relations = tuple.Right; - var factoriesMap = tuple.Left.Right; - var (fallbackSettings, definedSettings) = tuple.Left.Left.Right; - var partials = tuple.Left.Left.Left; - - var targetTypeModels = new List(); - - foreach(var (targetTypeSig, data) in partials) - { - var (representableTypes, isEqualsRequired, doesNotImplementToString) = data; - ct.ThrowIfCancellationRequested(); - - var settings = getSettings(targetTypeSig); - var relationAttributes = getRelations(targetTypeSig); - var factories = getFactories(targetTypeSig); - - var targetModel = UnionTypeModel.Create( - targetTypeSig, - representableTypes, - factories, - relationAttributes, - settings, - isEqualsRequired, - doesNotImplementToString, - ct); - - targetTypeModels.Add(targetModel); - } - - return new EquatableList(targetTypeModels); - - EquatableList getRelations(TypeSignatureModel target) => - relations!.TryGetValue(target, out var r) ? - r : - EquatableList.Empty; - SettingsModel getSettings(TypeSignatureModel targetTypeSig) => - definedSettings.TryGetValue(targetTypeSig, out var settings) ? - settings : - fallbackSettings; - EquatableList getFactories(TypeSignatureModel targetTypeSig) => - factoriesMap!.TryGetValue(targetTypeSig, out var factories) - ? factories - : EquatableList.Empty; - }).SelectMany((models, ct) => - { - ct.ThrowIfCancellationRequested(); - return models; - }); - private static PartialTargetTypeModelUnionProvider UnionPartialTypeModelProviders(PartialUnionTypeModelsProvider provider) => - provider - .Select((keyValuePairs, ct) => - { - ct.ThrowIfCancellationRequested(); - - var mutableResult = new Dictionary representableTypes, HashSet mappedRepresentableTypes)>(); - var result = new Dictionary representableTypes, Boolean isEqualsRequired, Boolean doesNotImplementToString)>(); - var isEqualsRequiredAccumulators = new Dictionary(); - var doesNotImplementToStringAccumulators = new Dictionary(); - - foreach(var (key, value, isEqualsRequired, doesNotImplementToString, _) in keyValuePairs) - { - if(!isEqualsRequiredAccumulators.ContainsKey(key)) - isEqualsRequiredAccumulators[key] = true; - - if(!doesNotImplementToStringAccumulators.ContainsKey(key)) - doesNotImplementToStringAccumulators[key] = true; - - isEqualsRequiredAccumulators[key] &= isEqualsRequired; - doesNotImplementToStringAccumulators[key] &= doesNotImplementToString; - - ct.ThrowIfCancellationRequested(); - if(!mutableResult.TryGetValue(key, out var data)) - { - data = ([], []); - mutableResult.Add(key, data); - result.Add(key, (new(data.representableTypes), isEqualsRequiredAccumulators[key], doesNotImplementToStringAccumulators[key])); - } else - { - result[key] = (result[key].representableTypes, isEqualsRequiredAccumulators[key], doesNotImplementToStringAccumulators[key]); - } - - if(data.mappedRepresentableTypes.Add(value.Signature)) - data.representableTypes.Add(value); - } - - return new EquatableDictionary representableTypes, Boolean isEqualsRequired, Boolean doesNotImplementToString)>(result); - }); - #endregion - #region Relations FAWMN - private static RelationsProvider CreateRelationsProvider(IncrementalGeneratorInitializationContext context) - { - var result = Enumerable.Range(2, 8) - .Select(i => $"RhoMicro.CodeAnalysis.RelationAttribute`{i}") - .Select(createProvider) - .Aggregate( - createProvider("RhoMicro.CodeAnalysis.RelationAttribute`1"), - (leftProvider, rightProvider) => - leftProvider.Combine(rightProvider) - .Select((tuple, ct) => - { - ct.ThrowIfCancellationRequested(); - var (leftList, rightList) = tuple; - var result = leftList.Concat(rightList) - .GroupBy(t => t.targetSignature) - .Select(g => (targetSignature: g.Key, relations: g.SelectMany(t => t.relations).ToEquatableList(ct))) - .ToEquatableList(ct); - - return result; - })) - .Select((tuples, ct) => - { - ct.ThrowIfCancellationRequested(); - - var mutableResult = new Dictionary>(); - var result = new Dictionary>(); - for(var i = 0; i < tuples.Count; i++) - { - ct.ThrowIfCancellationRequested(); - - var (targetSignature, relations) = tuples[i]; - if(!mutableResult.TryGetValue(targetSignature, out var models)) - { - models = []; - mutableResult.Add(targetSignature, models); - result.Add(targetSignature, new(models)); - } - - models.AddRange(relations); - } - - //Map(UnionTypeSignature, List(Relation)) - return result.AsEquatable(); - }); - - return result; - - IncrementalValueProvider relations)>> createProvider(String name) => - context.SyntaxProvider.ForAttributeWithMetadataName< - (TypeSignatureModel targetSignature, EquatableList relations)?>( - name, - Qualifications.IsUnionTypeDeclarationSyntax, - (ctx, ct) => - { - ct.ThrowIfCancellationRequested(); - //check if target of [Relation] is regular struct or class - if(ctx.TargetSymbol is not INamedTypeSymbol target || - target.TypeKind is not TypeKind.Struct and not TypeKind.Class || - target.IsRecord) - { - return null; - } - - ct.ThrowIfCancellationRequested(); - var targetSignature = TypeSignatureModel.Create(target, ct); - var relations = new List(); - //check if T in [Relation] is named type & is actual union type (has attribute on self or type param) <== consider marker attribute on generated impl? - for(var i = 0; i < ctx.Attributes.Length; i++) - { - ct.ThrowIfCancellationRequested(); - if(ctx.Attributes[i].AttributeClass?.TypeArguments.Length is null or < 1) - continue; - - foreach(var potentialRelationSymbol in ctx.Attributes[i].AttributeClass!.TypeArguments) - { - ct.ThrowIfCancellationRequested(); - if(potentialRelationSymbol is INamedTypeSymbol relationSymbol && - relationSymbol.GetAttributes() - .OfAliasedUnionTypeBaseAttribute() - .Concat(relationSymbol.TypeParameters - .SelectMany(p => p.GetAttributes().OfAliasedUnionTypeBaseAttribute())) - .Any()) - { - var model = RelationModel.Create(target, relationSymbol, ct); - relations.Add(model); - } - } - } - - return (targetSignature, relations.AsEquatable()); - }).Where(m => m.HasValue && m.Value.relations.Count > 0) - .Select((t, ct) => - { - ct.ThrowIfCancellationRequested(); - return t!.Value; - }) - .Collect() - .WithCollectionComparer() - .Select((tuples, ct) => - { - ct.ThrowIfCancellationRequested(); - return tuples.AsEquatable(); - }); - } - #endregion - #region Settings FAWMN - private static IncrementalValueProvider<(SettingsAttributeData data, SettingsModel parsed)> CreateAssemblySettingsProvider(IncrementalGeneratorInitializationContext context) => - context.SyntaxProvider.ForAttributeWithMetadataName( - UnionTypeSettingsAttribute.MetadataName, - (node, ct) => node is CompilationUnitSyntax, - (ctx, ct) => - { - ct.ThrowIfCancellationRequested(); - var result = SettingsAttributeData.CreateAssemblySettingsWithDefaults(ctx.Attributes[0]); - return result; - }) - .Collect() - .WithCollectionComparer() - .Select((attributes, ct) => - { - ct.ThrowIfCancellationRequested(); - var result = attributes.Length > 0 ? - attributes[0] : - SettingsAttributeData.Default; - - return result; - }).Select((data, ct) => - { - ct.ThrowIfCancellationRequested(); - var attribute = UnionTypeSettingsAttribute.TryCreate(data, out var a) ? - a : - new(); - - ct.ThrowIfCancellationRequested(); - var settings = SettingsModel.Create(attribute!); - - return (data, settings); - }); - private static SettingsMapProvider CreateSettingsProvider(IncrementalGeneratorInitializationContext context) => - context.SyntaxProvider.ForAttributeWithMetadataName< - (TypeSignatureModel? targetSignature, SettingsAttributeData Settings)>( - UnionTypeSettingsAttribute.MetadataName, - Qualifications.IsUnionTypeDeclarationSyntax, - (ctx, ct) => - { - ct.ThrowIfCancellationRequested(); - var settings = SettingsAttributeData.CreateDeclaredSettings(ctx, ct); - var targetSignature = ctx.TargetSymbol is ITypeSymbol type ? - TypeSignatureModel.Create(type, ct) : - null; - - var result = (targetSignature, settings); - - return result; - }).Where(t => t.targetSignature != null) - .Collect() - .WithCollectionComparer() - .Select((tuples, ct) => - { - //convert to equatable for combine result to be equatable - ct.ThrowIfCancellationRequested(); - return tuples.AsEquatable(); - }) - .Combine(CreateAssemblySettingsProvider(context)) - .Select((tuple, ct) => - { - ct.ThrowIfCancellationRequested(); - var (declaredSettingsTuples, assemblySettingsTuple) = tuple; - var (assemblySettingsData, assemblySettings) = assemblySettingsTuple; - - var definedSettings = new Dictionary(); - foreach(var (target, declaredSettings) in declaredSettingsTuples) - { - ct.ThrowIfCancellationRequested(); - - if(target == null || definedSettings.ContainsKey(target)) - continue; - - var defaultsAppliedSettings = SettingsAttributeData.CreateDeclaredSettingsWithDefaults( - declaredSettings, - assemblySettingsData); - - if(!UnionTypeSettingsAttribute.TryCreate(defaultsAppliedSettings, out var parsedSettings)) - continue; - - var settings = SettingsModel.Create(parsedSettings!); - - definedSettings.Add(target, settings); - } - - return (assemblySettings, definedSettings.AsEquatable()); - }); - #endregion - #region Factory FAWMN - private static FactoryMapProvider CreateFactoryMapProvider(IncrementalGeneratorInitializationContext context) => - context.SyntaxProvider.ForAttributeWithMetadataName< - (TypeSignatureModel targetSignature, TypeSignatureModel representableTypeSignature, FactoryModel factory)?>( - UnionTypeFactoryAttribute.MetadataName, - Qualifications.IsUnionTypeFactoryDeclarationSyntax, - (ctx, ct) => - { - //transform factory to (containing union type, representable type to create union with, factory model) - - ct.ThrowIfCancellationRequested(); - //check target is valid parameter in method symbol - if(ctx.TargetSymbol is not IParameterSymbol - { - ContainingSymbol: IMethodSymbol - { - ReturnType.NullableAnnotation: NullableAnnotation.NotAnnotated or NullableAnnotation.None - } - }) - { - return null; - } - - ct.ThrowIfCancellationRequested(); - //check method returns non nullable union type - var method = (IMethodSymbol)ctx.TargetSymbol.ContainingSymbol; - if(!method.ReturnType.Equals(method.ContainingType, SymbolEqualityComparer.Default)) - { - return null; - } - - ct.ThrowIfCancellationRequested(); - var targetSignature = TypeSignatureModel.Create(method.ContainingType, ct); - var representableType = ( (IParameterSymbol)ctx.TargetSymbol ).Type; - var representableTypeSignature = TypeSignatureModel.Create(representableType, ct); - var factory = FactoryModel.CreateCustom(method.Name, representableType, ct); - - return (targetSignature, representableTypeSignature, factory); - }).Where(t => t.HasValue) - .Collect() - .WithCollectionComparer() - .Select((tuples, ct) => - { - ct.ThrowIfCancellationRequested(); - - var mutableResult = new Dictionary set, List list)>(); - var result = new Dictionary>(); - for(var i = 0; i < tuples.Length; i++) - { - var (targetSignature, representableTypeSignature, factory) = tuples[i]!.Value; - if(!mutableResult.TryGetValue(targetSignature, out var mutableItem)) - { - mutableItem = ([], []); - mutableResult.Add(targetSignature, mutableItem); - result.Add(targetSignature, new(mutableItem.list)); - } - - if(mutableItem.set.Add(representableTypeSignature)) - mutableItem.list.Add(factory); - } - - //Map(UnionTypeSignature, Map(RepresentableTypeSignature, Factory)) - return result.AsEquatable(); - }); - #endregion - #region UnionType FAWMN - private static PartialUnionTypeModelsProvider CreateTypeDeclarationTargetProvider(IncrementalGeneratorInitializationContext context, String name) => - context.SyntaxProvider.ForAttributeWithMetadataName( - name, - Qualifications.IsUnionTypeDeclarationSyntax, - PartialUnionTypeModel.CreateFromTypeDeclaration) - .Collect() - .WithCollectionComparer() - .Select((models, ct) => - { - ct.ThrowIfCancellationRequested(); - var result = models.SelectMany(m => m).ToEquatableList(ct); - return result; - }); - private static PartialUnionTypeModelsProvider CreateTypeParameterTargetProvider(IncrementalGeneratorInitializationContext context) => - context.SyntaxProvider.ForAttributeWithMetadataName( - Qualifications.NonGenericFullMetadataName, - Qualifications.IsUnionTypeParameterSyntax, - PartialUnionTypeModel.CreateFromTypeParameter) - .Where(m => m != null) - .Collect() - .WithCollectionComparer() - .Select((models, ct) => - { - ct.ThrowIfCancellationRequested(); - return models.AsEquatable(); - })!; - private static PartialUnionTypeModelsProvider CreateUnionTypeProvider(IncrementalGeneratorInitializationContext context) => - Qualifications.GenericMetadataNames - .Select(name => CreateTypeDeclarationTargetProvider(context, name)) - .Aggregate( - CreateTypeParameterTargetProvider(context), - (leftProvider, rightProvider) => - leftProvider.Combine(rightProvider) - .Select((models, ct) => - { - ct.ThrowIfCancellationRequested(); - var result = models.Left.Concat(models.Right).ToEquatableList(ct); - return result; - })); - #endregion - #region Constant Sources - private static void RegisterConstantSources(IncrementalGeneratorPostInitializationContext context) - { - context.CancellationToken.ThrowIfCancellationRequested(); - context.AddSource("RhoMicro_CodeAnalysis_UnionTypeAttribute.g.cs", AliasedUnionTypeBaseAttribute.SourceText); - context.AddSource("RhoMicro_CodeAnalysis_RelationTypeAttribute.g.cs", RelationAttribute.SourceText); - context.AddSource("RhoMicro_CodeAnalysis_UnionFactoryAttribute.g.cs", UnionTypeFactoryAttribute.SourceText); - context.AddSource("RhoMicro_CodeAnalysis_UnionTypeSettingsAttribute.g.cs", UnionTypeSettingsAttribute.SourceText); - context.AddSource("RhoMicro_CodeAnalysis_UnionsGenerator_Generated_Util.g.cs", ConstantSources.Util); - } - #endregion -} diff --git a/UnionsGenerator/Models/FactoryModel.cs b/UnionsGenerator/Models/FactoryModel.cs deleted file mode 100644 index fd221d5..0000000 --- a/UnionsGenerator/Models/FactoryModel.cs +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Visitors; -using RhoMicro.CodeAnalysis.UnionsGenerator.Utils; - -internal sealed record FactoryModel( - Boolean RequiresGeneration, - String Name, - TypeSignatureModel Parameter, - EquatedData> Locations) : IModel -{ - public static EquatableList Create(INamedTypeSymbol targetSymbol, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var result = targetSymbol.GetMembers() - .OfType() - .Where(Qualifications.IsUnionTypeFactorySymbol) - .Select(m => CreateCustom(m.Name, m.Parameters[0].Type, cancellationToken)) - .ToEquatableList(cancellationToken); - - return result; - } - - private static FactoryModel CreateCustom(String name, ITypeSymbol parameterType, ImmutableArray location, CancellationToken cancellationToken) => - new(RequiresGeneration: false, name, TypeSignatureModel.Create(parameterType, cancellationToken), location); - public static FactoryModel CreateCustom(String name, ITypeSymbol parameterType, CancellationToken cancellationToken) => - CreateCustom(name, parameterType, ImmutableArray.Empty, cancellationToken); - public static FactoryModel CreateCustom(IMethodSymbol parameterType, CancellationToken cancellationToken) => - CreateCustom(parameterType.Name, parameterType.ContainingType ?? throw new ArgumentException($"Containing type of {nameof(parameterType)} must not be null.", nameof(parameterType)), parameterType.Locations, cancellationToken); - public static FactoryModel CreateGenerated(PartialRepresentableTypeModel model) => - new(RequiresGeneration: true, model.FactoryName , model.Signature, ImmutableArray.Empty); - public void Receive(TVisitor visitor) - where TVisitor : IVisitor - => visitor.Visit(this); -} diff --git a/UnionsGenerator/Models/GroupModel.cs b/UnionsGenerator/Models/GroupModel.cs deleted file mode 100644 index 58dc5a6..0000000 --- a/UnionsGenerator/Models/GroupModel.cs +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -using System; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Visitors; - -internal sealed record GroupModel(String Name, EquatableSet Members) : IModel -{ - public void Receive(TVisitor visitor) - where TVisitor : IVisitor - => visitor.Visit(this); -} diff --git a/UnionsGenerator/Models/GroupsModel.cs b/UnionsGenerator/Models/GroupsModel.cs deleted file mode 100644 index 65caac6..0000000 --- a/UnionsGenerator/Models/GroupsModel.cs +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models; -using System; -using System.Collections.Generic; -using System.Collections.Immutable; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Visitors; - -internal sealed class GroupsModel : IModel -{ - private GroupsModel(EquatableDictionary map) => _map = map; - private readonly EquatableDictionary _map; - - public static GroupsModel Create(EquatableList representableTypes) - { - var map = new Dictionary.Builder>(); - - foreach(var (group, representableType) in representableTypes.SelectMany(t => t.Groups.Select(g => (g, t)))) - { - if(!map.TryGetValue(group, out var signatures)) - { - signatures = ImmutableHashSet.CreateBuilder(); - map.Add(group, signatures); - } - - _ = signatures.Add(representableType); - } - - var resultMap = map - .Select(kvp => - ( - name: kvp.Key, - model: new GroupModel(kvp.Key, kvp.Value.ToImmutable().AsEquatable() - ))) - .ToDictionary(t => t.name, t => t.model) - .AsEquatable(); - - var result = new GroupsModel(resultMap); - - return result; - } - - public GroupModel this[String name] => _map[name]; - public GroupModel this[GroupModel group] => this[group.Name]; - public IEnumerable Groups => _map.Values; - public IEnumerable Names => _map.Keys; - - public override Boolean Equals(Object? obj) => - ReferenceEquals(obj, this) - || obj is GroupsModel model - && EqualityComparer>.Default.Equals(_map, model._map); - public override Int32 GetHashCode() => - -2013957080 + EqualityComparer>.Default.GetHashCode(_map); - public void Receive(TVisitor visitor) - where TVisitor : IVisitor - => visitor.Visit(this); - -} diff --git a/UnionsGenerator/Models/IModel.cs b/UnionsGenerator/Models/IModel.cs deleted file mode 100644 index 9c5bc40..0000000 --- a/UnionsGenerator/Models/IModel.cs +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Visitors; - -/// -/// Represents a base model, capable of receiving visitors for Open/Closed-compliant extension of functionality. -/// -/// The model type itself (CRTP). -public interface IModel -{ - /// - /// Receives a visitor. - /// - /// - /// Implementations should simply call the visitors - /// method, where T is .
- /// For example:
- /// - /// class MyModel : IModel<MyModel> - /// { - /// public void Receive<TVisitor>(TVisitor visitor) - /// where TVisitor : IVisitor<MyModel> - /// => visitor.Receive(this); - /// } - /// - ///
- /// The type of visitor to receive. - /// The visitor to receive. - void Receive(TVisitor visitor) - where TVisitor : IVisitor; -} diff --git a/UnionsGenerator/Models/NamedOrTypeParameterType.cs b/UnionsGenerator/Models/NamedOrTypeParameterType.cs deleted file mode 100644 index b12d6ec..0000000 --- a/UnionsGenerator/Models/NamedOrTypeParameterType.cs +++ /dev/null @@ -1,93 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -using Microsoft.CodeAnalysis; - -internal readonly struct TypeOrTypeParameterType : IEquatable -{ - private readonly ITypeSymbol? _type; - private readonly ITypeParameterSymbol? _typeParameter; - private readonly Byte _tag; - private const Int32 _namedTag = 1; - private const Int32 _typeParameterTag = 2; - - public TypeOrTypeParameterType(ITypeSymbol named) - { - _type = named; - _tag = _namedTag; - } - public TypeOrTypeParameterType(ITypeParameterSymbol typeParameter) - { - _typeParameter = typeParameter; - _tag = _typeParameterTag; - } - - public Boolean IsNamed => _tag == _namedTag; - public Boolean Is(out ITypeSymbol? named) - { - var result = _tag == _namedTag; - named = result ? _type! : default; - - return result; - } - public Boolean IsTypeParameter => _tag == _typeParameterTag; - public Boolean Is(out ITypeParameterSymbol? typeParameter) - { - var result = _tag == _typeParameterTag; - typeParameter = result ? _typeParameter! : default; - - return result; - } - - public T Match(Func onType, Func onTypeParameter) => - _tag switch - { - _namedTag => onType(_type!), - _typeParameterTag => onTypeParameter(_typeParameter!), - _ => throw new InvalidOperationException("The union type was not initialized correctly.") - }; - public ITypeSymbol UnifiedType => Match(n => n, p => p); - public void Switch(Action onNamed, Action onTypeParameter) - { - switch(_tag) - { - case _namedTag: - onNamed(_type!); - return; - case _typeParameterTag: - onTypeParameter(_typeParameter!); - return; - default: - throw new InvalidOperationException("The union type was not initialized correctly."); - } - } - - public override Boolean Equals(Object? obj) => - obj is TypeOrTypeParameterType type && Equals(type); - public Boolean Equals(TypeOrTypeParameterType other) => - _tag == other._tag && - _tag switch - { - _namedTag => SymbolEqualityComparer.Default.Equals(_type, other._type), - _typeParameterTag => SymbolEqualityComparer.Default.Equals(_typeParameter, other._typeParameter), - _ => throw new InvalidOperationException("The union type was not initialized correctly.") - }; - - public override Int32 GetHashCode() - { - var hashCode = -903911270; - hashCode = hashCode * -1521134295 + _tag.GetHashCode(); - hashCode = hashCode * -1521134295 + _tag switch - { - _namedTag => SymbolEqualityComparer.Default.GetHashCode(_type), - _typeParameterTag => SymbolEqualityComparer.Default.GetHashCode(_typeParameter), - _ => throw new InvalidOperationException("The union type was not initialized correctly.") - }; - ; - return hashCode; - } - - public static Boolean operator ==(TypeOrTypeParameterType left, TypeOrTypeParameterType right) => left.Equals(right); - public static Boolean operator !=(TypeOrTypeParameterType left, TypeOrTypeParameterType right) => !(left == right); -} diff --git a/UnionsGenerator/Models/PartialRepresentableTypeModel.cs b/UnionsGenerator/Models/PartialRepresentableTypeModel.cs deleted file mode 100644 index b5b16be..0000000 --- a/UnionsGenerator/Models/PartialRepresentableTypeModel.cs +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -using Microsoft.CodeAnalysis; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Visitors; -using RhoMicro.CodeAnalysis.UnionsGenerator.Utils; - -using System.Collections.Immutable; -using System.Threading; - -/// -/// Models a partial model of a union types representable type. -/// -/// -/// -/// -/// -/// -/// -/// -/// -internal record PartialRepresentableTypeModel( - String Alias, - String FactoryName, - UnionTypeOptions Options, - StorageOption Storage, - EquatableList Groups, - TypeSignatureModel Signature, - Boolean OmitConversionOperators, - Boolean IsBaseClassToUnionType) : - IModel -{ - public static PartialRepresentableTypeModel Create( - String? alias, - String factoryName, - UnionTypeOptions options, - StorageOption storage, - EquatableList groups, - TypeOrTypeParameterType representableType, - INamedTypeSymbol unionType, - CancellationToken cancellationToken) - { - Throw.ArgumentNull(groups, nameof(groups)); - - cancellationToken.ThrowIfCancellationRequested(); - var isNullableAnnotated = options.HasFlag(UnionTypeOptions.Nullable); - var typeModel = TypeSignatureModel.Create(representableType.UnifiedType, isNullableAnnotated, cancellationToken); - - var nonNullAlias = alias ?? typeModel.Names.IdentifierOrHintName; - - var omitConversionOperators = IsOperatorConversionsOmitted(representableType, unionType, out var isBaseClassToUnionType, cancellationToken); - - var result = new PartialRepresentableTypeModel( - nonNullAlias, - factoryName, - options, - storage, - groups, - typeModel, - omitConversionOperators, - isBaseClassToUnionType); - - return result; - } - private static Boolean IsOperatorConversionsOmitted(TypeOrTypeParameterType representableType, INamedTypeSymbol unionType, out Boolean unionTypeInheritsRepresentableType, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - unionTypeInheritsRepresentableType = InheritsFrom(unionType, representableType.UnifiedType, cancellationToken); - if(unionTypeInheritsRepresentableType) - return true; - - //cancellationToken.ThrowIfCancellationRequested(); - //if(representableType.IsTypeParameter) - // return true; - - cancellationToken.ThrowIfCancellationRequested(); - if(representableType.UnifiedType.TypeKind == TypeKind.Interface) - return true; - - return false; - } - private static Boolean InheritsFrom(ITypeSymbol subtype, ITypeSymbol supertype, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - var baseTypes = getBaseTypes(subtype); - if(baseTypes.Contains(supertype, SymbolEqualityComparer.Default)) - return true; - - var interfaces = subtype.AllInterfaces; - return interfaces.Contains(supertype, SymbolEqualityComparer.Default); - - IEnumerable getBaseTypes(ITypeSymbol symbol) - { - cancellationToken.ThrowIfCancellationRequested(); - - var baseType = symbol.BaseType; - while(baseType != null) - { - cancellationToken.ThrowIfCancellationRequested(); - - yield return baseType; - - baseType = baseType.BaseType; - } - } - } - /// - public virtual void Receive(TVisitor visitor) - where TVisitor : IVisitor - => visitor.Visit(this); -} diff --git a/UnionsGenerator/Models/PartialUnionTypeModel.cs b/UnionsGenerator/Models/PartialUnionTypeModel.cs deleted file mode 100644 index 710a4a6..0000000 --- a/UnionsGenerator/Models/PartialUnionTypeModel.cs +++ /dev/null @@ -1,185 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Visitors; -using RhoMicro.CodeAnalysis.UnionsGenerator.Utils; - -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.Threading; - -internal record PartialUnionTypeModel( - TypeSignatureModel Signature, - PartialRepresentableTypeModel RepresentableType, - Boolean IsEqualsRequired, - Boolean DoesNotImplementToString, - EquatedData> Locations) - : IModel -{ - public static EquatableList Create(INamedTypeSymbol unionType, CancellationToken cancellationToken) => - CreateFromTypeDeclaration(unionType, cancellationToken) - .Concat(unionType.TypeParameters.Select(p => - CreateFromTypeParameter(p, cancellationToken)) - .Where(m => m != null)) - .ToEquatableList(cancellationToken)!; - public static PartialUnionTypeModel? CreateFromTypeParameter(ITypeParameterSymbol target, CancellationToken cancellationToken) => - CreateFromTypeParameter( - target, - target.GetAttributes() - .Where(Qualifications.IsUnionTypeParameterAttribute) - .ToImmutableArray(), - target.ContainingType.Locations, - cancellationToken); - public static PartialUnionTypeModel? CreateFromTypeParameter(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) - { - //transform parent declaration of type parameter target of [UnionType] - - cancellationToken.ThrowIfCancellationRequested(); - //check that target is type param - if(context.TargetSymbol is not ITypeParameterSymbol typeParam) - return null; - - var locations = ImmutableArray.Empty; - var parent = context.TargetNode; - while(parent?.Parent is not null) - { - if(parent.Parent is TypeDeclarationSyntax tds) - { - locations = ImmutableArray.Create(tds.Identifier.GetLocation()); - break; - } - - parent = parent.Parent; - } - - var result = CreateFromTypeParameter( - typeParam, - context.Attributes, - locations, - cancellationToken); - - return result; - } - private static PartialUnionTypeModel? CreateFromTypeParameter(ITypeParameterSymbol target, ImmutableArray attributes, ImmutableArray targetLocations, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - if(attributes.Length < 1) - return null; - - cancellationToken.ThrowIfCancellationRequested(); - if(!AliasedUnionTypeBaseAttribute.TryCreate(attributes[0], out var attribute)) - return null; - - cancellationToken.ThrowIfCancellationRequested(); - var signature = TypeSignatureModel.Create(target.ContainingType, cancellationToken); - var representableType = attribute!.GetPartialModel(new(target), target.ContainingType, cancellationToken); - var containingType = target.ContainingType; - var isEqualsRequired = IsEqualsRequiredForTarget(containingType); - var doesNotImplementToString = TargetDoesNotImplementToString(containingType); - - var result = new PartialUnionTypeModel(signature, representableType, isEqualsRequired, doesNotImplementToString, targetLocations); - - return result; - } - - public static Boolean IsEqualsRequiredForTarget(INamedTypeSymbol target) => - !target - .GetMembers(nameof(Equals)) - .OfType() - .Any(m => m.Parameters.Length == 1 && - SymbolEqualityComparer.IncludeNullability.Equals( - target.WithNullableAnnotation(NullableAnnotation.Annotated), - m.Parameters[0].Type.WithNullableAnnotation(NullableAnnotation.Annotated))); - public static Boolean TargetDoesNotImplementToString(INamedTypeSymbol target) => - !target.GetMembers(nameof(ToString)) - .OfType() - .Any(m => m.Parameters.Length == 0); - - public static EquatableList CreateFromTypeDeclaration( - INamedTypeSymbol target, - CancellationToken cancellationToken) => - CreateFromTypeDeclaration( - target, - target.GetAttributes() - .Where(Qualifications.IsUnionTypeDeclarationAttribute) - .ToImmutableArray(), - cancellationToken); - public static EquatableList CreateFromTypeDeclaration(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) - { - //transform target of [UnionType] - - cancellationToken.ThrowIfCancellationRequested(); - //check that target of [UnionType] is regular struct or class - if(context.TargetSymbol is not INamedTypeSymbol target) - return EquatableList.Empty; - - var result = CreateFromTypeDeclaration(target, context.Attributes, cancellationToken); - - return result; - } - private static EquatableList CreateFromTypeDeclaration( - INamedTypeSymbol target, - ImmutableArray attributes, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - if(target.TypeKind is not TypeKind.Struct and not TypeKind.Class || - target.IsRecord) - { - return EquatableList.Empty; - } - - var result = attributes - .SelectMany(data => CreatePartials(data, target, cancellationToken)) - .ToEquatableList(cancellationToken); - - return result!; - } - - private static IEnumerable CreatePartials( - AttributeData attributeData, - INamedTypeSymbol target, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - cancellationToken.ThrowIfCancellationRequested(); - if(!AliasedUnionTypeBaseAttribute.TryCreate(attributeData, out var attribute)) - yield break; - - cancellationToken.ThrowIfCancellationRequested(); - if(attributeData.AttributeClass?.TypeArguments.Length is 0 or null) - yield break; - - var args = attributeData.AttributeClass.TypeArguments; - if(args.Length > 1) - { - //ignore invalid aliae for non-alializable attribute usages - attribute!.Alias = null; - } - - var signature = TypeSignatureModel.Create(target, cancellationToken); - var isEqualsRequired = IsEqualsRequiredForTarget(target); - var doesNotImplementToString = TargetDoesNotImplementToString(target); - - for(var i = 0; i < args.Length; i++) - { - cancellationToken.ThrowIfCancellationRequested(); - //check that T in [UnionType] is valid representable type - if(args[i] is INamedTypeSymbol named) - { - var representableType = attribute!.GetPartialModel(new(named), target, cancellationToken); - var locations = target.Locations; - yield return new(signature, representableType, isEqualsRequired, doesNotImplementToString, locations); - } - } - } - public void Receive(TVisitor visitor) - where TVisitor : IVisitor - => visitor.Visit(this); -} diff --git a/UnionsGenerator/Models/RelatedTypeModel.cs b/UnionsGenerator/Models/RelatedTypeModel.cs deleted file mode 100644 index 418a888..0000000 --- a/UnionsGenerator/Models/RelatedTypeModel.cs +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -using Microsoft.CodeAnalysis; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Models.Storage; -using RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Visitors; -using RhoMicro.CodeAnalysis.UnionsGenerator.Utils; - -internal sealed record RelatedTypeModel(TypeSignatureModel Signature, EquatableSet RepresentableTypeSignatures) : IModel -{ - public static RelatedTypeModel Create(INamedTypeSymbol relationSymbol, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - var representableTypeSignatures = GetRepresentableTypeSignatures(relationSymbol, cancellationToken); - var signature = TypeSignatureModel.Create(relationSymbol, cancellationToken); - var result = new RelatedTypeModel(signature, representableTypeSignatures); - - return result; - } - private static EquatableSet GetRepresentableTypeSignatures(INamedTypeSymbol relationSymbol, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - var declarationSignatures = PartialUnionTypeModel.CreateFromTypeDeclaration(relationSymbol, cancellationToken); - var typeParamSignatures = relationSymbol.TypeParameters - .Select(p => PartialUnionTypeModel.CreateFromTypeParameter(p, cancellationToken)) - .Where(m => m != null); - - var relationTypeSignature = TypeSignatureModel.Create(relationSymbol, cancellationToken); - - var result = declarationSignatures.Concat(typeParamSignatures) - .Where(m => m!.Signature.Equals(relationTypeSignature)) - .Where(p => p != null) - .Select(p => p!.RepresentableType.Signature) - .Distinct() - .ToEquatableSet(cancellationToken); - - return result; - } - public void Receive(TVisitor visitor) - where TVisitor : IVisitor - => visitor.Visit(this); -} diff --git a/UnionsGenerator/Models/RelationModel.cs b/UnionsGenerator/Models/RelationModel.cs deleted file mode 100644 index ab40d68..0000000 --- a/UnionsGenerator/Models/RelationModel.cs +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -using Microsoft.CodeAnalysis; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Visitors; -using RhoMicro.CodeAnalysis.UnionsGenerator.Utils; - -using System; -using System.Collections.Immutable; - -internal readonly record struct RelationModel(RelatedTypeModel RelatedType, RelationType RelationType) : IModel -{ - public static EquatableList Create(INamedTypeSymbol targetSymbol, CancellationToken cancellationToken) => - targetSymbol.GetAttributes() - .Where(Qualifications.IsRelationAttribute) - .Where(a => a.AttributeClass != null) - .SelectMany(a => a.AttributeClass!.TypeArguments) - .OfType() - .Select(t => Create(targetSymbol, t, cancellationToken)) - .ToEquatableList(cancellationToken); - public static RelationModel Create( - INamedTypeSymbol targetSymbol, - INamedTypeSymbol relationSymbol, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - var relatedType = RelatedTypeModel.Create(relationSymbol, cancellationToken); - var relationType = GetRelationType(targetSymbol, relationSymbol, relatedType, cancellationToken); - - var result = new RelationModel(relatedType, relationType); - - return result; - } - private static RelationType GetRelationType( - INamedTypeSymbol targetType, - INamedTypeSymbol relatedType, - RelatedTypeModel relatedTypeModel, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var isBidirectional = relatedType - .GetAttributes() - .Where(a => - a.AttributeClass?.MetadataName == RelationAttribute.MetadataName && - a.AttributeClass.TypeArguments.Length == 1 && - SymbolEqualityComparer.Default.Equals( - a.AttributeClass.TypeArguments[0], - targetType)) - .Any(); - if(isBidirectional) - return RelationType.BidirectionalRelation; - - var relationTypes = relatedTypeModel.RepresentableTypeSignatures - .Select(s => s.Names.FullGenericNullableName) - .ToList(); - var targetTypes = RelatedTypeModel.Create(targetType, cancellationToken).RepresentableTypeSignatures - .Select(s => s.Names.FullGenericNullableName) - .ToList(); - - //is target subset of relation? - if(targetTypes.All(relationTypes.Contains)) - { - //is target congruent to relation? - return relationTypes.Count == targetTypes.Count ? - RelationType.Congruent : - RelationType.Subset; - } - - //is target superset of relation? - if(relationTypes.All(targetTypes.Contains)) - return RelationType.Superset; - - //is relation intersection of target - return relationTypes.Any(targetTypes.Contains) ? RelationType.Intersection : RelationType.Disjunct; - } - public void Receive(TVisitor visitor) - where TVisitor : IVisitor - => visitor.Visit(this); -} diff --git a/UnionsGenerator/Models/RelationType.cs b/UnionsGenerator/Models/RelationType.cs deleted file mode 100644 index aa41aa6..0000000 --- a/UnionsGenerator/Models/RelationType.cs +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -/// -/// Defines the type of relations that can be defined between two union types. -/// -internal enum RelationType -{ - /// - /// There is no relation between the provided type and target type. - /// They do not share any representable types. - /// No conversion operators will be generated. - /// - Disjunct, - /// - /// The relation is defined on both the target type as well as the relation type. - /// Only for one of the two union types will conversion operators be generated. - /// - BidirectionalRelation, - /// - /// The target type is a superset of the provided type. - /// The target type may represent all of the provided types representable types. - /// This means that two conversion operations will be generated: - /// - /// - /// an implicit conversion operator from the provided type to the target type - /// - /// - /// an explicit conversion operator from the target type to the provided type - /// - /// - /// This option is not available if the provided type has already defined a relation to the target type. - /// - Superset, - /// - /// The target type is a subset of the provided type. - /// The provided type may represent all of the target types representable types. - /// This means that two conversion operations will be generated: - /// - /// - /// an implicit conversion operator from the target type to the provided type - /// - /// - /// an explicit conversion operator from the provided type to the target type - /// - /// - /// This option is not available if the provided type has already defined a relation to the target type. - /// - Subset, - /// - /// The target type intersects the provided type. - /// The target type may represent some, but not all of the provided types representable types; and vice-versa. - /// This means that two conversion operations will be generated: - /// - /// - /// an explicit conversion operator from the target type to the provided type - /// - /// - /// an explicit conversion operator from the provided type to the target type - /// - /// - /// This option is not available if the provided type has already defined a relation to the target type. - /// - Intersection, - /// - /// The target type is congruent to the provided type. - /// The target type may represent all of the provided types representable types; and vice-versa. - /// This means that two conversion operations will be generated: - /// - /// - /// an implicit conversion operator from the target type to the provided type - /// - /// - /// an implicit conversion operator from the provided type to the target type - /// - /// - /// This option is not available if the provided type has already defined a relation to the target type. - /// - Congruent -} diff --git a/UnionsGenerator/Models/RepresentableTypeModel.cs b/UnionsGenerator/Models/RepresentableTypeModel.cs deleted file mode 100644 index 52d7eeb..0000000 --- a/UnionsGenerator/Models/RepresentableTypeModel.cs +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -using System.Collections.Immutable; -using System.Threading; - -using Microsoft.CodeAnalysis; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Models.Storage; -using RhoMicro.CodeAnalysis.UnionsGenerator.Utils; - -/// -/// Models a representable type. -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -internal sealed record RepresentableTypeModel( - String Alias, - String FactoryName, - UnionTypeOptions Options, - StorageOption Storage, - EquatableList Groups, - TypeSignatureModel Signature, - Boolean OmitConversionOperators, - Boolean IsBaseClassToUnionType, - FactoryModel Factory, - EquatedData StorageStrategy) : - PartialRepresentableTypeModel(Alias, FactoryName, Options, Storage, Groups, Signature, OmitConversionOperators, IsBaseClassToUnionType) -{ - /// - /// Creates a new representable type model. - /// - /// - /// - /// - /// - /// - public static RepresentableTypeModel Create( - PartialRepresentableTypeModel partialModel, - FactoryModel factory, - StorageStrategy strategy, - CancellationToken cancellationToken) - { - Throw.ArgumentNull(partialModel, nameof(partialModel)); - - cancellationToken.ThrowIfCancellationRequested(); - var result = new RepresentableTypeModel( - partialModel.Alias, - partialModel.FactoryName, - partialModel.Options, - partialModel.Storage, - partialModel.Groups, - partialModel.Signature, - partialModel.OmitConversionOperators, - partialModel.IsBaseClassToUnionType, - factory, - strategy); - - return result; - } - /// - public override void Receive(TVisitor visitor) - => visitor.Visit(this); -} diff --git a/UnionsGenerator/Models/SettingsModel.cs b/UnionsGenerator/Models/SettingsModel.cs deleted file mode 100644 index 6caacbe..0000000 --- a/UnionsGenerator/Models/SettingsModel.cs +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -using System; -using System.Collections.Immutable; - -using Microsoft.CodeAnalysis; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Generators; -using RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Visitors; -using RhoMicro.CodeAnalysis.UnionsGenerator.Utils; - -internal sealed record SettingsModel( -#region Settings - ToStringSetting ToStringSetting, - LayoutSetting Layout, - DiagnosticsLevelSettings DiagnosticsLevel, - ConstructorAccessibilitySetting ConstructorAccessibility, - InterfaceMatchSetting InterfaceMatchSetting, - EqualityOperatorsSetting EqualityOperatorsSetting, - MiscellaneousSettings Miscellaneous, -#endregion -#region Strings - String TypeDeclarationPreface, - String GenericTValueName, - String TryConvertTypeName, - String MatchTypeName, - String TagTypeName, - String ValueTypeContainerTypeName, - String ValueTypeContainerName, - String ReferenceTypeContainerName, - String TagFieldName, - String TagNoneName, - String JsonConverterTypeName -#endregion - ) : IModel -{ - private EquatedData>? _reservedNames; - public Boolean IsReservedGenericTypeName(String name) => - ( _reservedNames ??= new([GenericTValueName, TryConvertTypeName, MatchTypeName]) ).Value.Contains(name); - - public static ImmutableHashSet PropertyNames { get; } = new[] - { -#region Settings - nameof(ToStringSetting), - nameof(Layout), - nameof(DiagnosticsLevel), - nameof(ConstructorAccessibility), - nameof(InterfaceMatchSetting), - nameof(EqualityOperatorsSetting), - nameof(Miscellaneous), -#endregion -#region Strings - nameof(TypeDeclarationPreface), - nameof(GenericTValueName), - nameof(TryConvertTypeName), - nameof(MatchTypeName), - nameof(TagTypeName), - nameof(ValueTypeContainerTypeName), - nameof(ValueTypeContainerName), - nameof(ReferenceTypeContainerName), - nameof(TagFieldName), - nameof(TagNoneName), - nameof(JsonConverterTypeName), -#endregion - }.ToImmutableHashSet(); - public static SettingsModel Create(INamedTypeSymbol type, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var assemblySettings = type.ContainingAssembly.GetAttributes() - .Where(Qualifications.IsUnionTypeSettingsAttribute) - .Select(SettingsAttributeData.CreateAssemblySettingsWithDefaults) - .FirstOrDefault() - ?? SettingsAttributeData.Default; - - cancellationToken.ThrowIfCancellationRequested(); - var settings = type.GetAttributes() - .Where(Qualifications.IsUnionTypeSettingsAttribute) - .Select(a => SettingsAttributeData.CreateDeclaredSettingsWithDefaults(a, assemblySettings)) - .OfUnionTypeSettingsAttribute() - .SingleOrDefault() - ?? new(); - - cancellationToken.ThrowIfCancellationRequested(); - var result = Create(settings); - - return result; - } - public static SettingsModel Create(UnionTypeSettingsAttribute attribute) => new( - #region Settings - ToStringSetting: attribute.ToStringSetting, - Layout: attribute.Layout, - DiagnosticsLevel: attribute.DiagnosticsLevel, - ConstructorAccessibility: attribute.ConstructorAccessibility, - InterfaceMatchSetting: attribute.InterfaceMatchSetting, - EqualityOperatorsSetting: attribute.EqualityOperatorsSetting, - Miscellaneous: attribute.Miscellaneous, - #endregion - #region Strings - TypeDeclarationPreface: attribute.TypeDeclarationPreface, - GenericTValueName: attribute.GenericTValueName, - TryConvertTypeName: attribute.TryConvertTypeName, - MatchTypeName: attribute.MatchTypeName, - TagTypeName: attribute.TagTypeName, - ValueTypeContainerTypeName: attribute.ValueTypeContainerTypeName, - ValueTypeContainerName: attribute.ValueTypeContainerName, - ReferenceTypeContainerName: attribute.ReferenceTypeContainerName, - TagFieldName: attribute.TagFieldName, - TagNoneName: attribute.TagNoneName, - JsonConverterTypeName: attribute.JsonConverterTypeName - #endregion - ); - public void Receive(TVisitor visitor) - where TVisitor : IVisitor - => visitor.Visit(this); -} diff --git a/UnionsGenerator/Models/SymbolDisplayFormats.cs b/UnionsGenerator/Models/SymbolDisplayFormats.cs deleted file mode 100644 index db6c771..0000000 --- a/UnionsGenerator/Models/SymbolDisplayFormats.cs +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -using Microsoft.CodeAnalysis; - -internal static class SymbolDisplayFormats -{ - public static SymbolDisplayFormat FullNullableNoContainingTypesOrNamespacesVariant { get; } = - new(SymbolDisplayGlobalNamespaceStyle.Omitted, - SymbolDisplayTypeQualificationStyle.NameOnly, - SymbolDisplayGenericsOptions.IncludeTypeParameters | SymbolDisplayGenericsOptions.IncludeVariance, - SymbolDisplayMemberOptions.None, - SymbolDisplayDelegateStyle.NameOnly, - SymbolDisplayExtensionMethodStyle.Default, - SymbolDisplayParameterOptions.None, - SymbolDisplayPropertyStyle.NameOnly, - SymbolDisplayLocalOptions.None, - SymbolDisplayKindOptions.None, - SymbolDisplayMiscellaneousOptions.ExpandNullable | - SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier | - SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers); - public static SymbolDisplayFormat FullNullableNoContainingTypesOrNamespaces { get; } = - new(SymbolDisplayGlobalNamespaceStyle.Omitted, - SymbolDisplayTypeQualificationStyle.NameOnly, - SymbolDisplayGenericsOptions.IncludeTypeParameters, - SymbolDisplayMemberOptions.None, - SymbolDisplayDelegateStyle.NameOnly, - SymbolDisplayExtensionMethodStyle.Default, - SymbolDisplayParameterOptions.None, - SymbolDisplayPropertyStyle.NameOnly, - SymbolDisplayLocalOptions.None, - SymbolDisplayKindOptions.None, - SymbolDisplayMiscellaneousOptions.ExpandNullable | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier); - public static SymbolDisplayFormat FullNullable { get; } = - SymbolDisplayFormat.FullyQualifiedFormat - .WithMiscellaneousOptions( - /* - get rid of special types - - 10110 - NAND 00100 - => 10010 - - 10110 - &! 00100 - => 10010 - - 00100 - ^ 11111 - => 11011 - - 10110 - & 11011 - => 10010 - */ - ( SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions & - ( SymbolDisplayMiscellaneousOptions.UseSpecialTypes ^ (SymbolDisplayMiscellaneousOptions)Int32.MaxValue ) ) | - SymbolDisplayMiscellaneousOptions.ExpandNullable) - .WithGenericsOptions(SymbolDisplayGenericsOptions.IncludeTypeParameters) - .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted); -} diff --git a/UnionsGenerator/Models/TypeNamesModel.cs b/UnionsGenerator/Models/TypeNamesModel.cs deleted file mode 100644 index b6cd430..0000000 --- a/UnionsGenerator/Models/TypeNamesModel.cs +++ /dev/null @@ -1,236 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -using System; -using System.Data.SqlTypes; -using System.Security.AccessControl; - -using Microsoft.CodeAnalysis; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Visitors; -using RhoMicro.CodeAnalysis.UnionsGenerator.Utils; - -/// -/// Provides access to various required type name strings. -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -internal sealed class TypeNamesModel( - Lazy containingTypesStringLazy, - Lazy fullGenericNameLazy, - Lazy fullGenericNullableNameLazy, - Lazy fullMetadataNameLazy, - Lazy fullOpenGenericNameLazy, - Lazy genericNameLazy, - Lazy identifierOrHintNameLazy, - Lazy fullIdentifierOrHintNameLazy, - Lazy openGenericNameLazy, - Lazy typeArgsStringLazy, - Lazy commentRefStringLazy, - String name, - String @namespace) : IModel, IEquatable -{ - /// - /// Gets the string required for cref comment use. - /// - public String CommentRefString => commentRefStringLazy.Value; - /// - /// Gets the chain of containing type strings. - /// - public String ContainingTypesString => containingTypesStringLazy.Value; - /// - /// Gets the fully qualified, generic name. - /// - public String FullGenericName => fullGenericNameLazy.Value; - /// - /// Gets the fully qualified, generic name, appending a nullable ?, if the type has been declared as a nullable reference type. - /// - public String FullGenericNullableName => fullGenericNullableNameLazy.Value; - /// - /// Gets the fully qualified metadata name. - /// - public String FullMetadataName => fullMetadataNameLazy.Value; - /// - /// Gets the fully qualified, open generic name. - /// - public String FullOpenGenericName => fullOpenGenericNameLazy.Value; - /// - /// Gets the generic name. - /// - public String GenericName => genericNameLazy.Value; - /// - /// Gets a name suitable for identifiers or hints. - /// - public String IdentifierOrHintName => identifierOrHintNameLazy.Value; - /// - /// Gets a fully qualified name suitable for identifiers or hints. - /// - public String FullIdentifierOrHintName => fullIdentifierOrHintNameLazy.Value; - /// - /// Gets the open generic name. - /// - public String OpenGenericName => openGenericNameLazy.Value; - /// - /// Gets a comma-delimited list of generic arguments, enclosed in angled brackets. - /// - public String TypeArgsString => typeArgsStringLazy.Value; - - /// - /// Gets the type name. - /// - public String Name { get; } = name; - /// - /// Gets the types full enclosing namespace. - /// - public String Namespace { get; } = @namespace; - - internal void Reify() - { - _ = CommentRefString; - _ = ContainingTypesString; - _ = FullGenericName; - _ = FullMetadataName; - _ = FullOpenGenericName; - _ = GenericName; - _ = IdentifierOrHintName; - _ = FullIdentifierOrHintName; - _ = OpenGenericName; - _ = TypeArgsString; - } - - /// - /// Creates a new type names model. - /// - /// - /// - /// - /// - /// - /// - public static TypeNamesModel Create( - ITypeSymbol symbol, - EquatableList containingTypes, - EquatableList typeArgs, - Boolean isNullableAnnotated, - CancellationToken cancellationToken) - { - Throw.ArgumentNull(symbol, nameof(symbol)); - Throw.ArgumentNull(containingTypes, nameof(containingTypes)); - Throw.ArgumentNull(typeArgs, nameof(typeArgs)); - - cancellationToken.ThrowIfCancellationRequested(); - - var name = symbol.Name; - var @namespace = symbol.ContainingNamespace.IsGlobalNamespace ? - String.Empty : - symbol.ContainingNamespace.ToDisplayString(SymbolDisplayFormats.FullNullable); - - var namespacePeriod = String.IsNullOrEmpty(@namespace) ? - String.Empty : $"{@namespace}."; - - Lazy containingTypesString, typeArgsString, - fullMetadataName, fullOpenGenericName, - genericName, openGenericName, - fullGenericName, commentRefString, - identifierOrHintName, fullIdentifierOrHintName, - - containingTypesPeriod, openTypesParamString, - - fullGenericNullableName; - - containingTypesString = new(() => String.Join(".", containingTypes.Select(t => t.Names.GenericName))); - typeArgsString = new(() => typeArgs.Count > 0 ? - $"<{String.Join(", ", typeArgs.Select(a => a.IsTypeParameter ? a.Names.Name : a.Names.FullGenericName))}>" : - String.Empty); - - containingTypesPeriod = new(() => containingTypes.Count != 0 ? $"{containingTypesString.Value}." : String.Empty); - - genericName = new(() => symbol is ITypeParameterSymbol ? symbol.Name : $"{name}{typeArgsString.Value}"); - fullGenericName = new(() => symbol is ITypeParameterSymbol ? symbol.Name : $"{namespacePeriod}{containingTypesPeriod.Value}{genericName.Value}"); - - openTypesParamString = new(() => typeArgs.Count != 0 ? - $"<{String.Concat(Enumerable.Repeat(',', typeArgs.Count - 1))}>" : - String.Empty); - - openGenericName = new(() => symbol is ITypeParameterSymbol ? String.Empty : $"{name}{openTypesParamString.Value}"); - fullOpenGenericName = new(() => symbol is ITypeParameterSymbol ? String.Empty : $"{namespacePeriod}{containingTypesPeriod.Value}{openGenericName.Value}"); - - fullMetadataName = new(() => - { - var typeParamIndex = symbol is ITypeParameterSymbol p ? - $"!{p.ContainingType.TypeParameters.IndexOf(p, 0, SymbolEqualityComparer.IncludeNullability)}" : - String.Empty; - return containingTypes.Count > 0 ? - $"{containingTypes[^1].Names.FullMetadataName}{( symbol is ITypeParameterSymbol ? typeParamIndex : $"+{symbol.MetadataName}" )}" : - $"{( symbol is ITypeParameterSymbol ? String.Empty : namespacePeriod )}{( symbol is ITypeParameterSymbol ? typeParamIndex : symbol.MetadataName )}"; - }); - identifierOrHintName = new(() => symbol.ToDisplayString( - SymbolDisplayFormats.FullNullableNoContainingTypesOrNamespaces - .WithMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.UseAsterisksInMultiDimensionalArrays | SymbolDisplayMiscellaneousOptions.ExpandNullable)) - .Replace(" ", String.Empty) - .Replace("<", "_of_") - .Replace(">", String.Empty) - .Replace("*,", String.Empty) - .Replace("[", "_array") - .Replace("]", String.Empty) - .Replace("*", String.Empty) - .Replace(",", "_and_")); - fullIdentifierOrHintName = new(() => $"{namespacePeriod.Replace(".", "_")}{( containingTypes.Count != 0 ? $"{String.Join("_", containingTypes.Select(t => t.Names.IdentifierOrHintName))}_" : String.Empty )}{identifierOrHintName.Value}"); - - fullGenericNullableName = - isNullableAnnotated && symbol.IsReferenceType - ? new(() => $"{fullGenericName.Value}?") - : fullGenericName; - - commentRefString = new(() => fullGenericName.Value.Replace("<", "{").Replace(">", "}")); - - var result = new TypeNamesModel( - containingTypesStringLazy: containingTypesString, - fullGenericNameLazy: fullGenericName, - fullGenericNullableNameLazy: fullGenericNullableName, - fullMetadataNameLazy: fullMetadataName, - fullOpenGenericNameLazy: fullOpenGenericName, - genericNameLazy: genericName, - identifierOrHintNameLazy: identifierOrHintName, - fullIdentifierOrHintNameLazy: fullIdentifierOrHintName, - openGenericNameLazy: openGenericName, - typeArgsStringLazy: typeArgsString, - commentRefStringLazy: commentRefString, - name: name, - @namespace: @namespace); - - return result; - } - /// - public override String ToString() => FullGenericName; - /// - public override Boolean Equals(Object? obj) => Equals(obj as TypeNamesModel); - /// - public Boolean Equals(TypeNamesModel? other) => - ReferenceEquals(this, other) || - other is not null - && FullGenericName == other.FullGenericNullableName; - /// - public override Int32 GetHashCode() - { - var hashCode = -43426669; - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(FullGenericNullableName); - return hashCode; - } - /// - public void Receive(TVisitor visitor) - where TVisitor : IVisitor - => visitor.Visit(this); -} diff --git a/UnionsGenerator/Models/TypeNature.cs b/UnionsGenerator/Models/TypeNature.cs deleted file mode 100644 index 5d3a600..0000000 --- a/UnionsGenerator/Models/TypeNature.cs +++ /dev/null @@ -1,125 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -using Microsoft.CodeAnalysis; - -using System.Collections.Concurrent; - -/// -/// Enumerates atypes structural nature. -/// -// ATTENTION: order influences type order => changes are breaking -public enum TypeNature -{ - /// - /// The types nature is unknown. - /// - UnknownType, - /// - /// The type is known to be a reference type. - /// - ReferenceType, - /// - /// The type is known to be a value type that contains reference or impure value types. - /// - ImpureValueType, - /// - /// The type is known to contain only pure value types, and no reference types. - /// - PureValueType -} - -internal static class TypeNatures -{ - public static TypeNature Create(TypeOrTypeParameterType type, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - if(type.UnifiedType.IsPureValueType(cancellationToken)) - return TypeNature.PureValueType; - - return type.UnifiedType.IsValueType - ? TypeNature.ImpureValueType - : type.UnifiedType.IsReferenceType - ? TypeNature.ReferenceType - : TypeNature.UnknownType; - } - - private static readonly ConcurrentDictionary _valueTypeCache = new(SymbolEqualityComparer.Default); - - private static Boolean IsPureValueType(this ITypeSymbol symbol, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - evaluate(symbol, cancellationToken); - - if(!_valueTypeCache[symbol].HasValue) - throw new Exception($"Unable to determine whether {symbol.Name} is value type."); - - var result = _valueTypeCache[symbol]!.Value; - - return result; - - static void evaluate(ITypeSymbol symbol, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - if(_valueTypeCache.TryGetValue(symbol, out var currentResult)) - { - //cache could be initialized but undefined (null) - if(currentResult.HasValue) - //cache was not null - return; - } else - { - //initialize cache for type - _valueTypeCache[symbol] = null; - } - - if(symbol is ITypeParameterSymbol typeParam) - { - //The enum constraint guarantees an underlying integral type. - //Therefore, enum constrained type params will be pure. - //Otherwise, there is no way to determine purity. - _valueTypeCache[symbol] = - typeParam.ConstraintTypes.Any(t => - t.MetadataName == "Enum" && - t.ContainingNamespace.Name == "System"); - return; - } - - if(!symbol.IsValueType) - { - _valueTypeCache[symbol] = false; - return; - } - - var members = symbol.GetMembers(); - foreach(var member in members) - { - cancellationToken.ThrowIfCancellationRequested(); - - if(member is IFieldSymbol field && !field.IsStatic) - { - //is field type uninitialized in cache? - if(!_valueTypeCache.ContainsKey(field.Type)) - //initialize & define - evaluate(field.Type, cancellationToken); - - var fieldTypeIsValueType = _valueTypeCache[field.Type]; - if(fieldTypeIsValueType.HasValue && !fieldTypeIsValueType.Value) - { - //field type was initialized but found not to be value type - //apply transitive property - _valueTypeCache[symbol] = false; - return; - } - } - } - - //no issues found :) - _valueTypeCache[symbol] = true; - } - } -} diff --git a/UnionsGenerator/Models/TypeSignatureModel.cs b/UnionsGenerator/Models/TypeSignatureModel.cs deleted file mode 100644 index 973a154..0000000 --- a/UnionsGenerator/Models/TypeSignatureModel.cs +++ /dev/null @@ -1,208 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -using Microsoft.CodeAnalysis; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Visitors; -using RhoMicro.CodeAnalysis.Library; - -using System.Threading; -using System.Reflection; - -/// -/// Represents a types signature. -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -internal sealed record TypeSignatureModel( - EquatableList ContainingTypes, - EquatableList TypeArgs, - String DeclarationKeyword, - Boolean IsTypeParameter, - Boolean IsGenericType, - Boolean IsInterface, - Boolean IsStatic, - Boolean IsRecord, - Boolean HasNoBaseClass, - TypeNature Nature, - Boolean IsNullableAnnotated, - TypeNamesModel Names) -{ - private Int32 _reifiedState = 0; - /// - /// Creates a new type signature model from a type symbol. - /// - /// - /// - /// - public static TypeSignatureModel Create(ITypeSymbol type, CancellationToken cancellationToken) - => Create(type, isNullableAnnotated: type is { IsReferenceType: true, NullableAnnotation: NullableAnnotation.Annotated }, cancellationToken); - /// - /// Creates a new type signature model from a type symbol. - /// - /// - /// - /// - /// - public static TypeSignatureModel Create(ITypeSymbol type, Boolean isNullableAnnotated, CancellationToken cancellationToken) - { - var result = Create(type, new(SymbolEqualityComparer.IncludeNullability), isNullableAnnotated, cancellationToken); - - result.Reify(); - - return result; - } - private static TypeSignatureModel Create( - ITypeSymbol type, - Dictionary cache, - Boolean isNullableAnnotated, - CancellationToken cancellationToken) - { - if(cache.TryGetValue(type, out var result)) - return result; - - var declarationKeyword = type switch - { - { IsRecord: true, TypeKind: TypeKind.Class } => "record class", - { IsRecord: true, TypeKind: TypeKind.Struct } => "record struct", - { TypeKind: TypeKind.Class } => "class", - { TypeKind: TypeKind.Struct } => "struct", - { TypeKind: TypeKind.Interface } => "interface", - _ => String.Empty - }; - var nature = TypeNatures.Create(new(type), cancellationToken); - var isInterface = type.TypeKind == TypeKind.Interface; - var isTypeParameter = type is ITypeParameterSymbol; - var isStatic = type.IsStatic; - var isRecord = type.IsRecord; - var isGenericType = type is INamedTypeSymbol { TypeParameters.Length: > 0 }; - var hasNoBaseClass = - type.BaseType is null or - { - SpecialType: SpecialType.System_Object or - SpecialType.System_ValueType or - SpecialType.System_Enum - }; - var containingTypes = new List(); - var equatableContainingTypes = containingTypes.AsEquatable(); - var typeArgs = new List(); - var equatableTypeArgs = typeArgs.AsEquatable(); - var names = TypeNamesModel.Create(type, equatableContainingTypes, equatableTypeArgs, isNullableAnnotated, cancellationToken); - - result = new( - ContainingTypes: equatableContainingTypes, - TypeArgs: equatableTypeArgs, - DeclarationKeyword: declarationKeyword, - IsTypeParameter: isTypeParameter, - IsGenericType: isGenericType, - IsInterface: isInterface, - IsStatic: isStatic, - IsRecord: isRecord, - HasNoBaseClass: hasNoBaseClass, - Nature: nature, - IsNullableAnnotated: isNullableAnnotated, - Names: names); - - cache.Add(type, result); - - GetContainingTypeSignatures(type, containingTypes, cache, cancellationToken); - - GetTypeArgs(type, typeArgs, cache, cancellationToken); - - return result; - } - private static void GetContainingTypeSignatures( - ITypeSymbol type, - List signatures, - Dictionary cache, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - appendSignature(type.ContainingType); - - void appendSignature(ITypeSymbol? symbol) - { - cancellationToken.ThrowIfCancellationRequested(); - - if(symbol == null) - return; - - appendSignature(symbol.ContainingType); - - var isNullableAnnotated = symbol is { IsReferenceType: true, NullableAnnotation: NullableAnnotation.Annotated }; - var signature = Create(symbol, cache, isNullableAnnotated, cancellationToken); - signatures.Add(signature); - } - } - private static void GetTypeArgs( - ITypeSymbol type, - List args, - Dictionary cache, - CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - if(type is not INamedTypeSymbol named) - return; - - for(var i = 0; i < named.TypeArguments.Length; i++) - { - cancellationToken.ThrowIfCancellationRequested(); - - var typeArgument = named.TypeArguments[i]; - var isNullableAnnotated = typeArgument is { IsReferenceType: true, NullableAnnotation: NullableAnnotation.Annotated }; - var arg = Create(typeArgument, cache, isNullableAnnotated, cancellationToken); - args.Add(arg); - } - } - - private void Reify() - { - if(Interlocked.CompareExchange(ref _reifiedState, 1, 0) != 0) - return; - - Names.Reify(); - - for(var i = 0; i < ContainingTypes.Count; i++) - ContainingTypes[i].Reify(); - - for(var i = 0; i < TypeArgs.Count; i++) - TypeArgs[i].Reify(); - } - public override String ToString() => $"{Nature} {Names}"; - /// - public Boolean Equals(TypeSignatureModel? other) => - ReferenceEquals(this, other) || - other != null && - Nature == other.Nature && - IsStatic == other.IsStatic && - IsRecord == other.IsRecord && - IsTypeParameter == other.IsTypeParameter && - EqualityComparer.Default.Equals(Names, other.Names); - /// - public override Int32 GetHashCode() - { - var hashCode = -1817968017; - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(DeclarationKeyword); - hashCode = hashCode * -1521134295 + IsTypeParameter.GetHashCode(); - hashCode = hashCode * -1521134295 + Nature.GetHashCode(); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Names); - return hashCode; - } - /// - public void Receive(TVisitor visitor) - where TVisitor : IVisitor - => visitor.Visit(this); -} diff --git a/UnionsGenerator/Models/UnionTypeModel.cs b/UnionsGenerator/Models/UnionTypeModel.cs deleted file mode 100644 index 6ee1efb..0000000 --- a/UnionsGenerator/Models/UnionTypeModel.cs +++ /dev/null @@ -1,200 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Storage; -using RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Visitors; -using RhoMicro.CodeAnalysis.UnionsGenerator.Models.Storage; -using RhoMicro.CodeAnalysis.UnionsGenerator.Utils; -using Microsoft.CodeAnalysis; -using System.Collections.Immutable; - -/// -/// Represents a user-defined union type. -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// -internal sealed record UnionTypeModel( - TypeSignatureModel Signature, - EquatableList RepresentableTypes, - EquatableList Relations, - SettingsModel Settings, - Boolean IsGenericType, - String ScopedDataTypeName, - GroupsModel Groups, - EquatedData StrategyHostContainer, - Boolean IsEqualsRequired, - Boolean IsToStringRequired, - EquatedData> Locations) : IModel -{ - public static UnionTypeModel Create(INamedTypeSymbol unionType, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var signature = TypeSignatureModel.Create(unionType, cancellationToken); - var relations = RelationModel.Create(unionType, cancellationToken); - var settings = SettingsModel.Create(unionType, cancellationToken); - var factories = FactoryModel.Create(unionType, cancellationToken); - var isEqualsRequired = PartialUnionTypeModel.IsEqualsRequiredForTarget(unionType); - var isToStringRequired = PartialUnionTypeModel.TargetDoesNotImplementToString(unionType) && settings.ToStringSetting != ToStringSetting.None; - var partials = PartialUnionTypeModel.Create(unionType, cancellationToken) - .Select(m => m.RepresentableType) - .ToEquatableList(cancellationToken); - var locations = unionType.Locations - .Where(l => l.IsInSource && !( l.SourceTree?.FilePath.EndsWith(".g.cs") ?? true )) - .ToImmutableArray(); - - var result = Create( - signature, - partials, - factories, - relations, - settings, - isEqualsRequired, - isToStringRequired, - locations, - cancellationToken); - - return result; - } - /// - public void Receive(TVisitor visitor) - where TVisitor : IVisitor - => visitor.Visit(this); - public static UnionTypeModel Create( - TypeSignatureModel signature, - EquatableList partialRepresentableTypes, - EquatableList factories, - EquatableList relations, - SettingsModel settings, - Boolean isEqualsRequired, - Boolean doesNotImplementToString, - CancellationToken cancellationToken) => - Create( - signature, - partialRepresentableTypes, - factories, - relations, - settings, - isEqualsRequired, - doesNotImplementToString && settings.ToStringSetting != ToStringSetting.None, - ImmutableArray.Empty, - cancellationToken); - - /// - /// Creates a new union type model. - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static UnionTypeModel Create( - TypeSignatureModel signature, - EquatableList partialRepresentableTypes, - EquatableList factories, - EquatableList relations, - SettingsModel settings, - Boolean isEqualsRequired, - Boolean isToStringRequired, - ImmutableArray locations, - CancellationToken cancellationToken) - { - Throw.ArgumentNull(signature, nameof(signature)); - Throw.ArgumentNull(partialRepresentableTypes, nameof(partialRepresentableTypes)); - Throw.ArgumentNull(factories, nameof(factories)); - Throw.ArgumentNull(relations, nameof(relations)); - Throw.ArgumentNull(settings, nameof(settings)); - - cancellationToken.ThrowIfCancellationRequested(); - - var isGenericType = signature.TypeArgs.Count > 0; - var conversionFunctionsTypeName = $"{signature.Names.FullIdentifierOrHintName}_ScopedData{signature.Names.TypeArgsString}"; - - var factoryMap = factories.ToDictionary(f => f.Parameter); - var representableTypes = GetRepresentableTypes(partialRepresentableTypes, settings, isGenericType, factoryMap, cancellationToken); - var groups = GroupsModel.Create(representableTypes); - var hostContainer = CreateHostContainerAndReceiveStrategies(signature, settings, isGenericType, representableTypes); - - var result = new UnionTypeModel( - signature, - representableTypes, - relations, - settings, - isGenericType, - conversionFunctionsTypeName, - groups, - hostContainer, - isEqualsRequired, - isToStringRequired, - locations); - - return result; - } - - private static EquatableList GetRepresentableTypes(EquatableList partialRepresentableTypes, SettingsModel settings, Boolean isGenericType, Dictionary factoryMap, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var result = partialRepresentableTypes - .Select(p => - ( - partial: p, - factory: factoryMap.TryGetValue(p.Signature, out var f) - ? f - : FactoryModel.CreateGenerated(p), - strategy: StorageStrategy.Create(settings, isGenericType, p) - )).Select(t => RepresentableTypeModel.Create(t.partial, t.factory, t.strategy, cancellationToken)) - .ToEquatableList(cancellationToken); - - return result; - } - - private static StrategySourceHost CreateHostContainerAndReceiveStrategies(TypeSignatureModel signature, SettingsModel settings, Boolean isGenericType, EquatableList representableTypes) - { - var hostContainer = new StrategySourceHost( - settings, - signature, - isGenericType, - representableTypes); - - for(var i = 0; i < representableTypes.Count; i++) - { - representableTypes[i].StorageStrategy.Value.Visit(hostContainer); - } - - return hostContainer; - } - - public String GetSpecificAccessibility(TypeSignatureModel _) - { - var accessibility = Settings.ConstructorAccessibility; - - if(accessibility == ConstructorAccessibilitySetting.PublicIfInconvertible - //TODO: consider omissions - /*&& OperatorOmissions.AllOmissions.Contains(representableType)*/) - { - accessibility = ConstructorAccessibilitySetting.Public; - } - - var result = accessibility == ConstructorAccessibilitySetting.Public ? - "public" : - "private"; - - return result; - } -} diff --git a/UnionsGenerator/Properties/launchSettings.json b/UnionsGenerator/Properties/launchSettings.json deleted file mode 100644 index b56fe1d..0000000 --- a/UnionsGenerator/Properties/launchSettings.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "profiles": { - "Tests": { - "commandName": "DebugRoslynComponent", - "targetProject": "..\\UnionsGenerator.EndToEnd.Tests\\UnionsGenerator.EndToEnd.Tests.csproj" - }, - "TestApp": { - "commandName": "DebugRoslynComponent", - "targetProject": "..\\TestApp\\TestApp.csproj" - }, - "DslGenerator": { - "commandName": "DebugRoslynComponent", - "targetProject": "..\\DslGenerator\\DslGenerator.csproj" - } - } -} \ No newline at end of file diff --git a/UnionsGenerator/Transformation/CommentBuilder.cs b/UnionsGenerator/Transformation/CommentBuilder.cs deleted file mode 100644 index 4fc27a9..0000000 --- a/UnionsGenerator/Transformation/CommentBuilder.cs +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.Library.Text; -using System; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -internal partial record CommentBuilder -{ - public IndentedStringBuilder SeeRef(UnionTypeModel model) => - SeeRef(model.Signature); - public IndentedStringBuilder SeeRef(TypeSignatureModel signature) => - SeeRef(signature.Names.CommentRefString); - public IndentedStringBuilder Ref(TypeSignatureModel typeSignature) => - typeSignature.IsTypeParameter ? - Builder.Comment.TypeParamRef(typeSignature.Names.CommentRefString) : - Builder.Comment.SeeRef(typeSignature.Names.CommentRefString); - public IndentedStringBuilder InternalUse(TypeSignatureModel signature) => - Builder.Comment.OpenRemarks() - .Append("This member is not intended for use by user code inside of or any code outside of ").Comment.Ref(signature).Append('.') - .CloseBlock(); -} diff --git a/UnionsGenerator/Transformation/IndentedStringBuilder.cs b/UnionsGenerator/Transformation/IndentedStringBuilder.cs deleted file mode 100644 index 593f3f4..0000000 --- a/UnionsGenerator/Transformation/IndentedStringBuilder.cs +++ /dev/null @@ -1,322 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.Library.Text; -using System; -using System.Collections.Immutable; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Models; -using RhoMicro.CodeAnalysis.UnionsGenerator.Utils; - -internal partial class IndentedStringBuilder -{ - public IndentedStringBuilder Typeof(TypeSignatureModel type, Boolean open = false) => - Append("typeof(").Append(type.IsTypeParameter ? type.Names.Name : open ? type.Names.FullOpenGenericName : type.Names.FullGenericName).Append(')'); - public IndentedStringBuilder UtilIsMarked(String typeExpression) => - Append("RhoMicro.CodeAnalysis.UnionsGenerator.Generated.Util.IsMarked(").Append(typeExpression).Append(")"); - public IndentedStringBuilder UtilGetFullString(String typeExpression) => - Append("RhoMicro.CodeAnalysis.UnionsGenerator.Generated.Util.GetFullString(").Append(typeExpression).Append(")"); - public IndentedStringBuilder UtilGetFullString(Action typeExpression) => - Append("RhoMicro.CodeAnalysis.UnionsGenerator.Generated.Util.GetFullString(").Append(typeExpression).Append(")"); - public IndentedStringBuilder UtilGetFullString(TypeSignatureModel type) => - Append("RhoMicro.CodeAnalysis.UnionsGenerator.Generated.Util.GetFullString(").Typeof(type).Append(")"); - public IndentedStringBuilder UtilUnsafeConvert( - String tFrom, - String tTo, - String valueExpression) => - Append("(RhoMicro.CodeAnalysis.UnionsGenerator.Generated.Util.UnsafeConvert<") - .Append(tFrom).Append(", ").Append(tTo).Append(">(").Append(valueExpression).Append("))"); - public IndentedStringBuilder UtilUnsafeConvert( - String tFrom, - TypeSignatureModel tTo, - String valueExpression) => - UtilUnsafeConvert(tFrom, tTo.IsTypeParameter ? tTo.Names.Name : tTo.Names.FullGenericName, valueExpression); - public IndentedStringBuilder TagSwitchExpr( - UnionTypeModel target, - Action caseExpr, - String instanceExpr = "this", - Action? specialDefault = null) - { - var representableTypes = target.RepresentableTypes; - - if(representableTypes.Count == 1) - { - caseExpr.Invoke(this, representableTypes[0]); - - return this; - } - - Append(instanceExpr).Append('.').Append(target.Settings.TagFieldName).Append(" switch") - .OpenBracesBlock() - .AppendJoinLines(representableTypes.Append(null), (b, t) => - { - if(t == null) - { - b.AppendCore("_ => "); - if(specialDefault != null) - { - specialDefault.Invoke(b); - } else - { - b.AppendCore(ConstantSources.InvalidTagStateThrow); - } - } else - { - b.Append(target.Settings.TagTypeName).Append('.').Append(t.Alias).AppendCore(" => "); - caseExpr.Invoke(b, t); - } - }) - .CloseBlockCore(); - - return this; - } - public IndentedStringBuilder TagSwitchExpr( - UnionTypeModel target, - ImmutableHashSet representableTypesSet, - Action caseExpr, - String instanceExpr, - Action? specialDefault = null) - { - if(representableTypesSet.Count == 1) - { - caseExpr.Invoke(this, representableTypesSet.Single()); - - return this; - } - - var aliasMap = target.RepresentableTypes.ToDictionary(t => t.Signature, t => t.Alias); - - Append(instanceExpr).Append('.').Append(target.Settings.TagFieldName).Append(" switch") - .OpenBracesBlock() - .AppendJoinLines(representableTypesSet.Append(null), (b, s) => - { - if(s == null) - { - var useSpecialDefault = specialDefault != null && representableTypesSet.Count != target.RepresentableTypes.Count; - if(useSpecialDefault) - { - b.Append("> (").Append(target.Settings.TagTypeName).Append(')') - .Append(target.RepresentableTypes.Count.ToString()).Append(" => ").Append(ConstantSources.InvalidTagStateThrow).AppendCore(','); - } - - b.AppendCore("_ => "); - if(useSpecialDefault) - { - specialDefault!.Invoke(b); - } else - { - b.AppendCore(ConstantSources.InvalidTagStateThrow); - } - } else - { - b.Append(target.Settings.TagTypeName).Append('.').Append(aliasMap[s]).AppendCore(" => "); - caseExpr.Invoke(b, s); - } - }) - .CloseBlockCore(); - - return this; - } - public IndentedStringBuilder TagSwitchStmt( - UnionTypeModel target, - Action caseExpr, - String instanceExpr = "this", - Action? specialDefault = null) - { - if(target.RepresentableTypes.Count == 1) - { - caseExpr.Invoke(this, target.RepresentableTypes[0]); - - return this; - } - - Append("switch(").Append(instanceExpr).Append('.').Append(target.Settings.TagFieldName).Append(')') - .OpenBracesBlock() - .AppendJoinLines( - StringOrChar.Empty, - target.RepresentableTypes.Append(null), - (b, t) => - { - if(t == null) - { - _ = b.Append("default:").OpenBracesBlock(); - - if(specialDefault != null) - { - specialDefault.Invoke(b); - } else - { - b.Append(ConstantSources.InvalidTagStateThrow).AppendCore(';'); - } - - CloseBlockCore(); - } else - { - _ = b.Append("case ").Append(target.Settings.TagTypeName).Append('.').Append(t.Alias).Append(":") - .OpenBracesBlock(); - caseExpr.Invoke(b, t); - CloseBlockCore(); - } - }) - .CloseBlockCore(); - - return this; - } - public IndentedStringBuilder FullMetadataNameSwitchExpr( - UnionTypeModel target, - String metadataNameExpr, - Action caseExpr, - Action defaultCase) - => FullMetadataNameSwitchExpr( - target, - b => b.Append(metadataNameExpr), - caseExpr, - defaultCase); - public IndentedStringBuilder FullMetadataNameSwitchExpr( - UnionTypeModel target, - Action metadataNameExpr, - Action caseExpr, - Action defaultCase) - { - if(target.RepresentableTypes.Count == 1) - { - caseExpr.Invoke(this, target.RepresentableTypes[0]); - - return this; - } - - Append(metadataNameExpr).Append(" switch") - .OpenBracesBlock() - .AppendJoinLines(target.RepresentableTypes.Append(null), (b, t) => - { - if(t == null) - { - b.AppendCore("_ => "); - defaultCase.Invoke(b); - } else - { - b.Append('"').Append(t.Signature.Names.FullMetadataName).AppendCore("\" => "); - caseExpr.Invoke(b, t); - } - }) - .CloseBlockCore(); - - return this; - } - public IndentedStringBuilder FullMetadataNameSwitchExpr( - ImmutableHashSet representableTypesSet, - Action metadataNameExpr, - Action caseExpr, - Action defaultCase) - { - if(representableTypesSet.Count == 1) - { - caseExpr.Invoke(this, representableTypesSet.Single()); - - return this; - } - - Append(metadataNameExpr).Append(" switch") - .OpenBracesBlock() - .AppendJoinLines(representableTypesSet.Append(null), (b, s) => - { - if(s == null) - { - b.AppendCore("_ => "); - defaultCase.Invoke(b); - } else - { - b.Append('"').Append(s.Names.FullMetadataName).AppendCore("\" => "); - caseExpr.Invoke(b, s); - } - }) - .CloseBlockCore(); - - return this; - } - public IndentedStringBuilder InvalidConversionThrow(String fromTypeNameExpression, String representedTypeNameExpression, String toTypeNameExpression) => - Append("throw new System.InvalidOperationException($\"Unable to convert from an instance of {") - .Append(fromTypeNameExpression) - .Append("} representing a value of type {") - .Append(representedTypeNameExpression) - .Append("} to an instance of {") - .Append(toTypeNameExpression) - .Append("}.\")"); - public IndentedStringBuilder MetadataNameSwitchStmt( - UnionTypeModel target, - Action typeExpr, - Action caseBody, - Action defaultBody) - { - var nonTypeParamCases = new List>(); - var typeParamCases = new List>(); - - foreach(var t in target.RepresentableTypes) - { - ( t.Signature.IsTypeParameter ? typeParamCases : nonTypeParamCases ) - .Add(t.Signature.IsTypeParameter ? - b => - { - _ = b.Append("if(metadataName == ").UtilGetFullString(t.Signature).Append(")") - .OpenBracesBlock(); - caseBody.Invoke(b, t); - b.CloseBlockCore(); - } - : - b => - { - _ = b.Append("case ") - .Append('"').Append(t.Signature.Names.FullGenericName).Append("\":") - .OpenBracesBlock(); - caseBody.Invoke(b, t); - b.CloseBlockCore(); - }); - } - - typeParamCases.Add(b => b.OpenBracesBlock().Append(defaultBody).CloseBlockCore()); - - Append("var metadataName = ").UtilGetFullString(typeExpr).AppendLine(';') - .Append("switch(metadataName)") - .OpenBracesBlock() - .AppendJoinLines(StringOrChar.Empty, nonTypeParamCases) - .Append("default:") - .OpenBracesBlock() - .AppendJoin(" else ", typeParamCases) - .CloseBlock() - .CloseBlockCore(); - - return this; - } - public IndentedStringBuilder ToUnionTypeConversion( - UnionTypeModel unionType, - RepresentableTypeModel unionTypeRepresentableType, - String relationTypeParameterName) => - Append('(').Append(unionType.Signature.Names.GenericName).Append('.').Append(unionTypeRepresentableType.Factory.Name) - .Append("((").Append(unionTypeRepresentableType.Signature.Names.FullGenericNullableName).Append(')').Append(relationTypeParameterName).Append("))"); - public IndentedStringBuilder ToRelatedTypeConversion( - RelatedTypeModel relatedType, - RepresentableTypeModel unionTypeRepresentableType, - String unionTypeParameterName) => - Append("((").Append(relatedType.Signature.Names.FullGenericNullableName).Append(')') - .Append(unionTypeRepresentableType.StorageStrategy.Value.StrongInstanceVariableExpression(unionTypeParameterName)).Append(')'); - public IndentedStringBuilder GeneratedUnnavigableInternalCode(UnionTypeModel model) => - GeneratedUnnavigableInternalCode(model.Signature); - public IndentedStringBuilder GeneratedUnnavigableInternalCode(TypeSignatureModel signature) => - Comment.InternalUse(signature) - .AppendLine(ConstantSources.GeneratedCode) - .AppendLine(ConstantSources.EditorBrowsableNever); - public IndentedStringBuilder AppendJoinLines( - IEnumerable values, - Action append) - => AppendJoinLines(values.Select(v => new IndentedStringBuilderAppendable(b => append.Invoke(b, v)))); - public IndentedStringBuilder AppendJoinLines( - StringOrChar separator, - IEnumerable values, - Action append) - => AppendJoinLines(separator, values.Select(v => new IndentedStringBuilderAppendable(b => append.Invoke(b, v)))); - public IndentedStringBuilder AppendJoin( - StringOrChar separator, - IEnumerable values, - Action append) - => AppendJoin(separator, values.Select(v => new IndentedStringBuilderAppendable(b => append.Invoke(b, v)))); - -} diff --git a/UnionsGenerator/Transformation/Storage/StorageSelectionViolation.cs b/UnionsGenerator/Transformation/Storage/StorageSelectionViolation.cs deleted file mode 100644 index e868b97..0000000 --- a/UnionsGenerator/Transformation/Storage/StorageSelectionViolation.cs +++ /dev/null @@ -1,71 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Storage; - -internal enum StorageSelectionViolation -{ - None, - /// - /// - /// TypePure Value - /// SelectedReference - /// ActualReference - /// Diagnosticwarn about definite boxing - /// - /// - PureValueReferenceSelection, - /// - /// - /// TypePure Value - /// SelectedValue - /// ActualField - /// Diagnosticignored due to generic type - /// - /// - PureValueValueSelectionGeneric, - /// - /// - /// TypeImpure Value - /// SelectedReference - /// ActualReference - /// Diagnosticwarn about definite boxing - /// - /// - ImpureValueReference, - /// - /// - /// TypeImpure Value - /// SelectedValue - /// ActualField - /// Diagnosticignored due to guaranteed tle - /// - /// - ImpureValueValue, - /// - /// - /// TypeReference - /// SelectedValue - /// ActualReference - /// Diagnosticignored due to guaranteed tle - /// - /// - ReferenceValue, - /// - /// - /// TypeUnknown - /// SelectedReference - /// ActualReference - /// Diagnosticwarn about possible boxing - /// - /// - UnknownReference, - /// - /// - /// TypeUnknown - /// SelectedValue - /// ActualField - /// Diagnosticwarn about possible tle - /// - /// - UnknownValue -} diff --git a/UnionsGenerator/Transformation/Storage/StorageStrategy.FieldContainerStrategy.cs b/UnionsGenerator/Transformation/Storage/StorageStrategy.FieldContainerStrategy.cs deleted file mode 100644 index e2bc7f5..0000000 --- a/UnionsGenerator/Transformation/Storage/StorageStrategy.FieldContainerStrategy.cs +++ /dev/null @@ -1,38 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models.Storage; - -using RhoMicro.CodeAnalysis.Library.Text; -using static RhoMicro.CodeAnalysis.Library.Text.IndentedStringBuilder.Appendables; - -using System; -using RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Storage; - -internal partial class StorageStrategy -{ - private sealed class FieldContainerStrategy( - SettingsModel settings, - PartialRepresentableTypeModel representableType, - StorageOption selectedOption, - StorageSelectionViolation violation) - : StorageStrategy(settings, representableType, selectedOption, violation) - { - public override StorageOption ActualOption => StorageOption.Field; - public override IndentedStringBuilderAppendable ConvertedInstanceVariableExpression( - String targetType, - String instance) => - UtilUnsafeConvert(RepresentableType.Signature.Names.FullGenericName, targetType, $"{instance}.{FieldName}{NullableFieldBang}"); - public override IndentedStringBuilderAppendable StrongInstanceVariableExpression( - String instance) => - new(b => _ = b.Operators + '(' + instance + '.' + FieldName + NullableFieldBang + ')'); - public override IndentedStringBuilderAppendable InstanceVariableAssignmentExpression( - String valueExpression, - String instance) => - new(b => _ = b.Operators + instance + '.' + FieldName + " = " + valueExpression); - public override IndentedStringBuilderAppendable InstanceVariableExpression( - String instance) => - StrongInstanceVariableExpression(instance); - - public override void Visit(StrategySourceHost host) => host.AddDedicatedField(this); - } -} diff --git a/UnionsGenerator/Transformation/Storage/StorageStrategy.ReferenceContainerStrategy.cs b/UnionsGenerator/Transformation/Storage/StorageStrategy.ReferenceContainerStrategy.cs deleted file mode 100644 index 32bb6a5..0000000 --- a/UnionsGenerator/Transformation/Storage/StorageStrategy.ReferenceContainerStrategy.cs +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models.Storage; - -using RhoMicro.CodeAnalysis.Library.Text; -using RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Storage; - -internal partial class StorageStrategy -{ - private sealed class ReferenceContainerStrategy( - SettingsModel settings, - PartialRepresentableTypeModel representableType, - StorageOption selectedOption, - StorageSelectionViolation violation) : StorageStrategy(settings, representableType, selectedOption, violation) - { - public override StorageOption ActualOption { get; } = StorageOption.Reference; - - public override IndentedStringBuilderAppendable ConvertedInstanceVariableExpression(String targetType, String instance) => - new(b => b.UtilUnsafeConvert(RepresentableType.Signature.Names.FullGenericNullableName, targetType, $"({RepresentableType.Signature.Names.FullGenericNullableName}){instance}.{Settings.ReferenceTypeContainerName}{NullableFieldBang}")); - public override IndentedStringBuilderAppendable InstanceVariableAssignmentExpression(String valueExpression, String instance) => - new(b => b.Append(instance).Append('.').Append(Settings.ReferenceTypeContainerName).Append(" = ").Append(valueExpression)); - public override IndentedStringBuilderAppendable InstanceVariableExpression(String instance) => - new(b => b.Append('(').Append(instance).Append('.').Append(Settings.ReferenceTypeContainerName).Append(NullableFieldBang).Append(')')); - public override IndentedStringBuilderAppendable StrongInstanceVariableExpression(String instance) => - new(b => b.Append("((").Append(RepresentableType.Signature.Names.FullGenericNullableName).Append(')').Append(instance).Append('.').Append(Settings.ReferenceTypeContainerName).Append(NullableFieldBang).Append(')')); - public override void Visit(StrategySourceHost host) => host.AddReferenceTypeContainerField(); - } -} diff --git a/UnionsGenerator/Transformation/Storage/StorageStrategy.ValueContainerStrategy.cs b/UnionsGenerator/Transformation/Storage/StorageStrategy.ValueContainerStrategy.cs deleted file mode 100644 index d4afffc..0000000 --- a/UnionsGenerator/Transformation/Storage/StorageStrategy.ValueContainerStrategy.cs +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models.Storage; - -using System; - -using RhoMicro.CodeAnalysis.Library.Text; -using RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Storage; - -internal partial class StorageStrategy -{ - private sealed class ValueContainerStrategy( - SettingsModel settings, - PartialRepresentableTypeModel representableType, - StorageOption selectedOption, - StorageSelectionViolation violation) - : StorageStrategy(settings, representableType, selectedOption, violation) - { - public override StorageOption ActualOption => StorageOption.Value; - public override IndentedStringBuilderAppendable ConvertedInstanceVariableExpression( - String targetType, - String instance) => - IndentedStringBuilder.Appendables.UtilUnsafeConvert( - RepresentableType.Signature.Names.FullGenericNullableName, - targetType, - $"{instance}.{Settings.ValueTypeContainerName}.{RepresentableType.Alias}"); - public override IndentedStringBuilderAppendable StrongInstanceVariableExpression( - String instance) => - new(b => _ = b.Operators + '(' + instance + '.' + Settings.ValueTypeContainerName + '.' + RepresentableType.Alias + ')'); - public override IndentedStringBuilderAppendable InstanceVariableAssignmentExpression( - String valueExpression, - String instance) => - new(b => _ = b.Operators + instance + '.' + Settings.ValueTypeContainerName + " = new(" + valueExpression + ')'); - public override IndentedStringBuilderAppendable InstanceVariableExpression( - String instance) => - StrongInstanceVariableExpression(instance); - - public override void Visit(StrategySourceHost host) - { - host.AddValueTypeContainerField(); - host.AddValueTypeContainerType(); - host.AddValueTypeContainerInstanceFieldAndCtor(this); - } - } -} diff --git a/UnionsGenerator/Transformation/Storage/StorageStrategy.cs b/UnionsGenerator/Transformation/Storage/StorageStrategy.cs deleted file mode 100644 index c871978..0000000 --- a/UnionsGenerator/Transformation/Storage/StorageStrategy.cs +++ /dev/null @@ -1,162 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Models.Storage; - -using System; - -using RhoMicro.CodeAnalysis.Library.Text; -using RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Storage; - -using static RhoMicro.CodeAnalysis.Library.Text.IndentedStringBuilder.Appendables; - -internal abstract partial class StorageStrategy -{ - #region Constructor - private StorageStrategy( - SettingsModel settings, - PartialRepresentableTypeModel representableType, - StorageOption selectedOption, - StorageSelectionViolation violation) - { - Settings = settings; - RepresentableType = representableType; - SelectedOption = selectedOption; - Violation = violation; - FieldName = representableType.Alias.ToGeneratedCamelCase(); - NullableFieldBang = RepresentableType.Options.HasFlag(UnionTypeOptions.Nullable) ? String.Empty : "!"; - NullableFieldQuestionMark = RepresentableType.Signature is { Nature: TypeNature.UnknownType or TypeNature.ReferenceType } ? "?" : String.Empty; - } - #endregion - #region Fields - public String FieldName { get; } - protected SettingsModel Settings { get; } - public PartialRepresentableTypeModel RepresentableType { get; } - public StorageOption SelectedOption { get; } - public abstract StorageOption ActualOption { get; } - public StorageSelectionViolation Violation { get; } - protected String NullableFieldBang { get; } - public String NullableFieldQuestionMark { get; } - #endregion - #region Factory - public static StorageStrategy Create( - SettingsModel settings, - Boolean unionTypeIsGeneric, - PartialRepresentableTypeModel representableType) - { - var selectedOption = representableType.Storage; - var result = representableType.Signature.Nature switch - { - TypeNature.PureValueType => createForPureValueType(), - TypeNature.ImpureValueType => createForImpureValueType(), - TypeNature.ReferenceType => createForReferenceType(), - _ => createForUnknownType(), - }; - - return result; - - StorageStrategy createReference(StorageSelectionViolation violation = StorageSelectionViolation.None) => - new ReferenceContainerStrategy(settings, representableType, selectedOption, violation); - StorageStrategy createValue(StorageSelectionViolation violation = StorageSelectionViolation.None) => - new ValueContainerStrategy(settings, representableType, selectedOption, violation); - StorageStrategy createField(StorageSelectionViolation violation = StorageSelectionViolation.None) => - new FieldContainerStrategy(settings, representableType, selectedOption, violation); - - /* - read tables like so: - type nature | selected strategy | generic strat(diag) : nongeneric strat(diag) - */ - - /* - PureValue Reference => reference(box) - Value => field(generic) : value - Field => field - Auto => field : value - */ - StorageStrategy createForPureValueType() => - selectedOption switch - { - StorageOption.Reference => createReference(StorageSelectionViolation.PureValueReferenceSelection), - StorageOption.Value => unionTypeIsGeneric ? createField(StorageSelectionViolation.PureValueValueSelectionGeneric) : createValue(), - StorageOption.Field => createField(), - _ => unionTypeIsGeneric ? createField() : createValue() - }; - /* - ImpureValue Reference => reference(box) - Value => field(tle) - Field => field - Auto => field - */ - StorageStrategy createForImpureValueType() => - selectedOption switch - { - StorageOption.Reference => createReference(StorageSelectionViolation.ImpureValueReference), - StorageOption.Value => createField(StorageSelectionViolation.ImpureValueValue), - StorageOption.Field => createField(), - _ => createField() - }; - /* - Reference Reference => reference - Value => reference(tle) - Field => field - Auto => reference - */ - StorageStrategy createForReferenceType() => - selectedOption switch - { - StorageOption.Reference => createReference(), - StorageOption.Value => createReference(StorageSelectionViolation.ReferenceValue), - StorageOption.Field => createField(), - _ => createReference() - }; - /* - Unknown Reference => reference(pbox) - Value => field(ptle) - Field => field - Auto => field - */ - StorageStrategy createForUnknownType() => - selectedOption switch - { - StorageOption.Reference => createReference(StorageSelectionViolation.UnknownReference), - StorageOption.Value => createField(StorageSelectionViolation.UnknownValue), - StorageOption.Field => createField(), - _ => createField() - }; - } - #endregion - #region Template Methods - public abstract IndentedStringBuilderAppendable InstanceVariableExpression(String instance); - public abstract IndentedStringBuilderAppendable StrongInstanceVariableExpression(String instance); - public abstract IndentedStringBuilderAppendable ConvertedInstanceVariableExpression(String targetType, String instance); - public abstract IndentedStringBuilderAppendable InstanceVariableAssignmentExpression(String valueExpression, String instance); - #endregion - - public IndentedStringBuilderAppendable EqualsInvocation(String instance, String otherInstance) => - new(b => _ = b.Operators + '(' + "System.Collections.Generic.EqualityComparer<" + RepresentableType.Signature.Names.FullGenericNullableName + - ">.Default.Equals(" + StrongInstanceVariableExpression(instance) + ", " + StrongInstanceVariableExpression(otherInstance) + "))"); - - public IndentedStringBuilderAppendable EqualsInvocation(String otherInstance) => EqualsInvocation("this", otherInstance); - - public IndentedStringBuilderAppendable InstanceVariableExpression() => InstanceVariableExpression("this"); - public IndentedStringBuilderAppendable GetHashCodeInvocation(String instance) => - new(b => _ = b.Operators + - "(System.Collections.Generic.EqualityComparer<" + RepresentableType.Signature.Names.FullGenericNullableName + ">.Default.GetHashCode(" + StrongInstanceVariableExpression(instance) + "))"); - public IndentedStringBuilderAppendable GetHashCodeInvocation() => - GetHashCodeInvocation("this"); - - public IndentedStringBuilderAppendable TypesafeInstanceVariableExpression() - => StrongInstanceVariableExpression("this"); - - public IndentedStringBuilderAppendable ConvertedInstanceVariableExpression(String targetType) - => ConvertedInstanceVariableExpression(targetType, "this"); - - public IndentedStringBuilderAppendable InstanceVariableAssignmentExpression(String valueExpression) - => InstanceVariableAssignmentExpression(valueExpression, "this"); - - public IndentedStringBuilderAppendable ToStringInvocation(String instance) => - new(b => _ = b.Operators + '(' + InstanceVariableExpression(instance) + NullableFieldQuestionMark + ".ToString() ?? System.String.Empty)"); - public IndentedStringBuilderAppendable ToStringInvocation() - => ToStringInvocation("this"); - - public abstract void Visit(StrategySourceHost host); -} diff --git a/UnionsGenerator/Transformation/Storage/StrategySourceHost.cs b/UnionsGenerator/Transformation/Storage/StrategySourceHost.cs deleted file mode 100644 index f6671a2..0000000 --- a/UnionsGenerator/Transformation/Storage/StrategySourceHost.cs +++ /dev/null @@ -1,175 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Storage; - -using RhoMicro.CodeAnalysis.Library; -using RhoMicro.CodeAnalysis.Library.Text; -using static RhoMicro.CodeAnalysis.Library.Text.IndentedStringBuilder.Appendables; - -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using RhoMicro.CodeAnalysis.UnionsGenerator.Models; -using RhoMicro.CodeAnalysis.UnionsGenerator.Models.Storage; -using RhoMicro.CodeAnalysis.UnionsGenerator.Utils; -using System.Reflection; - -internal sealed class StrategySourceHost( - SettingsModel settings, - TypeSignatureModel unionTypeSignature, - Boolean unionTypeIsGeneric, - EquatableList representableTypes) -{ - private readonly SettingsModel _settings = settings ?? throw new ArgumentNullException(nameof(settings)); - private readonly EquatableList _representableTypes = representableTypes ?? throw new ArgumentNullException(nameof(representableTypes)); - - private readonly List<(IndentedStringBuilderAppendable Appendable, String TypeName)> _dedicatedReferenceFieldAdditions = []; - private readonly List<(IndentedStringBuilderAppendable Appendable, String TypeName)> _dedicatedPureValueTypeFieldAdditions = []; - private readonly List<(IndentedStringBuilderAppendable Appendable, String TypeName)> _dedicatedImpureAndUnknownFieldAdditions = []; - public void AddDedicatedField(StorageStrategy strategy) - { - var fullName = strategy.RepresentableType.Signature.Names.FullGenericName; - ( strategy.RepresentableType.Signature.Nature switch - { - TypeNature.ReferenceType => _dedicatedReferenceFieldAdditions, - TypeNature.PureValueType => _dedicatedPureValueTypeFieldAdditions, - _ => _dedicatedImpureAndUnknownFieldAdditions - } ).Add( - (Appendable: new((b) => - { - _ = b - .Comment.OpenSummary() - .Append("Contains the value of instances of ").Comment.SeeRef(unionTypeSignature).Append(" representing an instance of ") - .Comment.Ref(strategy.RepresentableType.Signature) - .Append('.') - .CloseBlock() - .GeneratedUnnavigableInternalCode(unionTypeSignature) - .Append("private readonly ").Append(fullName).Append(strategy.NullableFieldQuestionMark).Append(' ').Append(strategy.FieldName).AppendLine(';'); - }), - TypeName: fullName)); - } - - public void DedicatedReferenceFields(IndentedStringBuilder builder) => - _dedicatedReferenceFieldAdditions.ForEach(t => t.Appendable.AppendTo(builder)); - public void DedicatedPureValueTypeFields(IndentedStringBuilder builder) => - _dedicatedPureValueTypeFieldAdditions.OrderByDescending(static t => - { - var pureValueType = Type.GetType(t.TypeName, false); - var size = pureValueType != null ? - Marshal.SizeOf(pureValueType) : - Int32.MaxValue; - return size; - }).ForEach(t => t.Appendable.AppendTo(builder)); - public void DedicatedImpureAndUnknownFields(IndentedStringBuilder builder) => - _dedicatedImpureAndUnknownFieldAdditions.ForEach(t => t.Appendable.AppendTo(builder)); - - private Boolean _referenceFieldRequired; - public void AddReferenceTypeContainerField() => _referenceFieldRequired = true; - public void ReferenceTypeContainerField(IndentedStringBuilder builder) - { - if(!_referenceFieldRequired) - return; - - _ = builder.Comment.OpenSummary() - .Append("Contains the value of instances of ").Comment.SeeRef(unionTypeSignature).Append(" representing one of these types:") - .Comment.OpenList("bullet"); - - var referenceTypes = representableTypes - .Select(t => t.StorageStrategy.Value) - .Where(s => s.ActualOption == StorageOption.Reference) - .Select(s => s.RepresentableType.Signature); - foreach(var referenceType in referenceTypes) - { - _ = builder.Comment.OpenItem() - .Comment.Ref(referenceType) - .CloseBlock(); - } - - _ = builder.CloseBlock() - .CloseBlock() - .GeneratedUnnavigableInternalCode(unionTypeSignature) - .Append("private readonly System.Object? ").Append(settings.ReferenceTypeContainerName).AppendLine(';'); - } - - private Boolean _valueTypeContainerTypeRequired; - public void AddValueTypeContainerType() => _valueTypeContainerTypeRequired = true; - public void AddValueTypeContainerField() => _valueTypeContainerTypeRequired = true; - private readonly List _valueTypeFieldAdditions = []; - - public void AddValueTypeContainerInstanceFieldAndCtor(StorageStrategy strategy) => - _valueTypeFieldAdditions.Add(new((b) => - { - var fullName = strategy.RepresentableType.Signature.Names.FullGenericName; - - _ = b.Comment.OpenSummary() - .Append("Contains the value of instances of ").Comment.SeeRef(unionTypeSignature).Append(" representing an instance of ") - .Comment.Ref(strategy.RepresentableType.Signature) - .Append('.') - .CloseBlock() - .Append(b => - { - if(!unionTypeIsGeneric) - b.Append("[System.Runtime.InteropServices.FieldOffset(0)]").AppendLineCore(); - }) - .Append("public readonly ").Append(fullName) - .Append(' ').Append(strategy.RepresentableType.Alias).AppendLine(';') - .Append("public ").Append(settings.ValueTypeContainerTypeName).Append('(') - .Append(fullName).Append(" value) => this.") - .Append(strategy.RepresentableType.Alias).AppendLine(" = value;"); - })); - - public void ValueTypeContainerField(IndentedStringBuilder builder) - { - if(!_valueTypeContainerTypeRequired) - return; - - _ = builder.Comment.OpenSummary() - .Append("Contains the value of instances of ").Comment.SeeRef(unionTypeSignature).Append(" representing one of these types:") - .Comment.OpenList("bullet"); - - var valueTypes = representableTypes - .Select(t => t.StorageStrategy.Value) - .Where(s => s.ActualOption == StorageOption.Value) - .Select(s => s.RepresentableType.Signature); - foreach(var valueType in valueTypes) - { - _ = builder.Comment.OpenItem() - .Comment.Ref(valueType) - .CloseBlock(); - } - - _ = builder.CloseBlock() - .CloseBlock() - .GeneratedUnnavigableInternalCode(unionTypeSignature) - .Append("private readonly ").Append(settings.ValueTypeContainerTypeName) - .Append(' ').Append(settings.ValueTypeContainerName).Append(';'); - } - public void ValueTypeContainerType(IndentedStringBuilder builder) - { - if(!_valueTypeContainerTypeRequired) - return; - - _ = builder.Comment.OpenSummary() - .Append("Helper type for storing value types efficiently.") - .CloseBlock() - .GeneratedUnnavigableInternalCode(unionTypeSignature); - - if(!unionTypeIsGeneric) - _ = builder.AppendLine("[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit)]"); - - _ = builder - .Append("readonly struct ").Append(settings.ValueTypeContainerTypeName) - .OpenBracesBlock() - .AppendJoinLines(StringOrChar.Empty, _valueTypeFieldAdditions) - .CloseBlock(); - } -} - -file static class EnumerableExtensions -{ - public static void ForEach(this IEnumerable values, Action action) - { - foreach(var value in values) - action.Invoke(value); - } -} diff --git a/UnionsGenerator/Transformation/Storage/StringExtensions.cs b/UnionsGenerator/Transformation/Storage/StringExtensions.cs deleted file mode 100644 index eef66ac..0000000 --- a/UnionsGenerator/Transformation/Storage/StringExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Storage; -using System; -using System.Collections.Generic; -using System.Text; -using System.Text.RegularExpressions; - -internal static class StringExtensions -{ - //source: https://stackoverflow.com/a/58853591 - private static readonly Regex _camelCasePattern = - new(@"([A-Z])([A-Z]+|[a-z0-9_]+)($|[A-Z]\w*)", RegexOptions.Compiled); - public static String ToCamelCase(this String value) - { - if(value.Length == 0) - return value; - - if(value.Length == 1) - return value.ToLowerInvariant(); - - //source: https://stackoverflow.com/a/58853591 - var result = _camelCasePattern.Replace( - value, - static m => $"{m.Groups[1].Value.ToLowerInvariant()}{m.Groups[2].Value.ToLowerInvariant()}{m.Groups[3].Value}"); - - return result; - } - public static String ToGeneratedCamelCase(this String value) - { - var result = $"__{value.ToCamelCase()}"; - - return result; - } -} diff --git a/UnionsGenerator/Transformation/Visitors/AppendableSourceText.cs b/UnionsGenerator/Transformation/Visitors/AppendableSourceText.cs deleted file mode 100644 index 3d6e69f..0000000 --- a/UnionsGenerator/Transformation/Visitors/AppendableSourceText.cs +++ /dev/null @@ -1,1174 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Visitors; - -using System.Collections.Immutable; -using System.Xml.Linq; - -using RhoMicro.CodeAnalysis.Library.Text; -using RhoMicro.CodeAnalysis.UnionsGenerator.Models; -using RhoMicro.CodeAnalysis.UnionsGenerator.Utils; - -using static Library.Text.IndentedStringBuilder.Appendables; - -internal sealed partial class AppendableSourceText(UnionTypeModel target) : IIndentedStringBuilderAppendable -{ - /// - public void AppendTo(IndentedStringBuilder builder) - { - Throw.ArgumentNull(builder, nameof(builder)); - - var signature = target.Signature; - - if(!String.IsNullOrEmpty(signature.Names.Namespace)) - builder.Append("namespace ").Append(signature.Names.Namespace).OpenBlockCore(Blocks.Braces(builder.Options.NewLine)); - - builder.AppendLine("using System.Linq;") - .AppendCore(ScopedData); - - foreach(var containingType in signature.ContainingTypes) - { - builder.Append("partial ").Append(containingType.DeclarationKeyword) - .Append(' ').Append(containingType.Names.GenericName).OpenBlockCore(Blocks.Braces(builder.Options.NewLine)); - } - - if(target.Settings.Miscellaneous.HasFlag(MiscellaneousSettings.GenerateJsonConverter)) - { - builder.Append("[System.Text.Json.Serialization.JsonConverter(typeof(") - .Append(target.Signature.Names.OpenGenericName) - .Append(".JsonConverter))]") - .AppendLineCore(); - } - - builder.Append("partial ").Append(signature.DeclarationKeyword).Append(' ').Append(signature.Names.GenericName) - .Append(" : global::System.IEquatable<").Append(signature.Names.GenericName).Append(b => - { - if(target.Signature.Nature == TypeNature.ReferenceType) - b.AppendCore('?'); - }).Append('>') - .OpenBracesBlock() - .Append(NestedTypes) - .Append(Constructors) - .Append(Fields) - .Append(Factories) - .Append(Switch) - .Append(Match) - .Append(RepresentedTypes) - .Append(IsAsProperties) - .Append(IsGroupProperties) - .Append(IsAsFunctions) - .Append(ToString) - .Append(GetHashCode) - .Append(Equals) - .Append(Conversions) - .CloseAllBlocksCore(); - //InterfaceIntersections, - } - public void IsGroupProperties(IndentedStringBuilder builder) - { - builder.OpenRegionBlock("Is Group Properties") - .Append(b => - b.AppendJoin( - StringOrChar.Empty, - target.Groups.Groups, - (b, group) => - { - var (name, members) = group; - b.Comment.OpenSummary() - .Comment.OpenParagraph() - .Append("Gets a value indicating whether the currently represented value is part of the ").Append(name).Append(" group.") - .CloseBlock() - .Comment.OpenParagraph() - .Append("The group encompasses values of the following types:") - .CloseBlock() - .Comment.OpenParagraph() - .Comment.OpenList("bullet") - .AppendJoin( - StringOrChar.Empty, - members, - (b, m) => b.Comment.OpenItem().Comment.Ref(m.Signature).CloseBlockCore()) - .CloseBlock() - .CloseBlock() - .CloseBlock() - .Append("public System.Boolean Is").Append(name).AppendLine("Group => ") - .TagSwitchExpr( - target, - (b, m) => b.Append(members.Contains(m) ? "true" : "false")) - .Append(';') - .AppendLineCore(); - })) - .CloseBlockCore(); - } - public void Conversions(IndentedStringBuilder builder) => - builder.OpenRegionBlock("Conversions") - .Append(RepresentableTypeConversions) - .Append(RelatedTypeConversions) - .CloseBlockCore(); - public void RepresentableTypeConversions(IndentedStringBuilder builder) - { -#pragma warning disable IDE0047 // Remove unnecessary parentheses - var convertableTypes = target.RepresentableTypes - .Where(t => !( t.OmitConversionOperators /*|| t.Signature.IsTypeParameter && t.Options.HasFlag(UnionTypeOptions.SupersetOfParameter) */)); -#pragma warning restore IDE0047 // Remove unnecessary parentheses - builder.OpenRegionBlock("Representable Type Conversions") - .AppendJoinLines( - StringOrChar.Empty, - convertableTypes, - (b, representableType) => - { - b.Comment.OpenSummary() - .Append("Converts an instance of the representable type ") - .Comment.Ref(representableType.Signature) - .Append(" to the union type ") - .Comment.Ref(target.Signature).Append('.') - .CloseBlock() - .Comment.OpenParam("value") - .Append("The value to convert.") - .CloseBlock() - .Comment.OpenReturns() - .Append("The union type instance.") - .CloseBlock() - .Append("public static implicit operator ").Append(target.Signature.Names.GenericName) - .Append('(') - .Append(representableType.Signature.Names.FullGenericNullableName).Append(" value) => ") - .Append(representableType.Factory.Name).Append("(value);") - .AppendLineCore(); - - var generateSolitaryExplicit = - target.RepresentableTypes.Count > 1 || - !representableType.Options.HasFlag(UnionTypeOptions.ImplicitConversionIfSolitary); - - if(generateSolitaryExplicit) - { - b.Comment.OpenSummary() - .Append("Converts an instance of the union type ") - .Comment.Ref(target.Signature) - .Append(" to the representable type ") - .Comment.Ref(representableType.Signature).Append('.') - .CloseBlock() - .Comment.OpenParam("union") - .Append("The union to convert.") - .CloseBlock() - .Comment.OpenReturns() - .Append("The represented value.") - .CloseBlock() - .Append("public static explicit operator ") - .Append(representableType.Signature.Names.FullGenericNullableName) - .Append('(') - .Append(target.Signature.Names.FullGenericName) - .AppendCore(" union) =>"); - - if(target.RepresentableTypes.Count > 1) - { - b.Append("union.").Append(target.Settings.TagFieldName).Append(" == ") - .Append(target.Settings.TagTypeName) - .Append('.') - .Append(representableType.Alias) - .Append('?') - .AppendLineCore(); - } - - b.AppendCore(representableType.StorageStrategy.Value.StrongInstanceVariableExpression("union")); - - if(target.RepresentableTypes.Count > 1) - { - _ = b.Append(':') - .InvalidConversionThrow( - fromTypeNameExpression: $"typeof({target.Signature.Names.GenericName})", - representedTypeNameExpression: "union.RepresentedType", - toTypeNameExpression: $"typeof({representableType.Signature.Names.FullGenericName})"); - } - - b.AppendCore(';'); - } else - { - b.Append("public static implicit operator ") - .Append(representableType.Signature.Names.FullGenericNullableName) - .Append('(') - .Append(target.Signature.Names.FullGenericName) - .Append(" union) => ") - .Append(representableType.StorageStrategy.Value.StrongInstanceVariableExpression("union")) - .Append(';') - .AppendLineCore(); - } - }) - .CloseBlockCore(); - } - public void RelatedTypeConversions(IndentedStringBuilder builder) - { - builder.OpenRegionBlock("Related Type Conversions") - .AppendJoinLines( - StringOrChar.Empty, - target.Relations.Where(r => r.RelationType != RelationType.Disjunct), - (b, relation) => - { - var representableTypesIntersectionSet = relation.RelatedType.RepresentableTypeSignatures - .Intersect(target.RepresentableTypes.Select(t => t.Signature)) - .ToImmutableHashSet(); - - var unionTypeRepresentableTypesMap = target.RepresentableTypes - .Where(t => representableTypesIntersectionSet.Contains(t.Signature)) - .ToDictionary(t => t.Signature); - - //conversion to model from relation - //public static _plicit operator Target(Relation relatedUnion) - var relationType = relation.RelationType; - b.OpenRegionBlock($"{relationType switch - { - RelationType.Congruent => "Congruency with ", - RelationType.Intersection => "Intersection with ", - RelationType.Superset => "Superset of ", - RelationType.Subset => "Subset of ", - _ => "Relation" - }} {relation.RelatedType.Signature.Names.GenericName}") - .Append("public static ") - .Append(relationType is RelationType.Congruent or RelationType.Superset ? "im" : "ex") - .Append("plicit operator ").Append(target.Signature.Names.GenericName) - .Append('(') - .Append(relation.RelatedType.Signature.Names.FullGenericName) - .AppendLine(" relatedUnion) =>") - .Indent() - .Append(b => - { - _ = b.FullMetadataNameSwitchExpr( - representableTypesSet: representableTypesIntersectionSet, - metadataNameExpr: b => b.UtilGetFullString($"relatedUnion.RepresentedType"), - caseExpr: (b, representableTypeSignature) => - b.ToUnionTypeConversion( - unionType: target, - unionTypeRepresentableType: unionTypeRepresentableTypesMap[representableTypeSignature], - relationTypeParameterName: "relatedUnion"), - defaultCase: b => b.InvalidConversionThrow( - fromTypeNameExpression: $"typeof({relation.RelatedType.Signature.Names.FullGenericName})", - representedTypeNameExpression: "relatedUnion.RepresentedType", - toTypeNameExpression: $"typeof({target.Signature.Names.GenericName})")); - }) - .AppendLine(';') - .DetentCore(); - - //conversion to relation from model - //public static _plicit operator Relation(Target relatedUnion) - b.Append("public static ") - .Append(relationType is RelationType.Congruent or RelationType.Subset ? "im" : "ex") - .Append("plicit operator ").Append(relation.RelatedType.Signature.Names.FullGenericName) - .Append('(') - .Append(target.Signature.Names.FullGenericName) - .AppendLine(" union) =>") - .Indent() - .Append(b => - { - _ = b.TagSwitchExpr( - target, - representableTypesIntersectionSet, - (b, representableTypeSignature) => - b.ToRelatedTypeConversion( - relatedType: relation.RelatedType, - unionTypeRepresentableType: unionTypeRepresentableTypesMap[representableTypeSignature], - unionTypeParameterName: "union"), - instanceExpr: "union", - specialDefault: b => b.InvalidConversionThrow( - fromTypeNameExpression: $"typeof({target.Signature.Names.FullGenericName})", - representedTypeNameExpression: "union.RepresentedType", - toTypeNameExpression: $"typeof({relation.RelatedType.Signature.Names.GenericName})")); - }) - .AppendLine(';') - .DetentCore(); - b.CloseBlockCore(); - }) - .CloseBlockCore(); - } - public void Equals(IndentedStringBuilder builder) - { - builder.OpenRegionBlock("Equality") - .Comment.InheritDoc() - .AppendLine("public override System.Boolean Equals(System.Object? obj) =>") - .Indent() - .Append("obj is ").Append(target.Signature.Names.GenericName).AppendLine(" union && Equals(union);") - .Detent() - .Append(b => - { - if(!target.IsEqualsRequired) - return; - - b.Comment.InheritDoc() - .Append("public System.Boolean Equals(").Append(target.Signature.Names.GenericName).Append(b => - { - if(target.Signature.Nature == TypeNature.ReferenceType) - b.AppendCore('?'); - }).AppendLine(" other) =>") - .Indent() - .Append(b => - { - if(target.Signature.Nature == TypeNature.ReferenceType) - { - b.AppendLine("ReferenceEquals(other, this)") - .AppendLine("|| other != null") - .AppendCore("&& "); - } - - if(target.RepresentableTypes.Count > 1) - { - b.Append("this.").Append(target.Settings.TagFieldName) - .Append(" == other.") - .AppendLine(target.Settings.TagFieldName) - .AppendCore("&& "); - } - }).TagSwitchExpr( - target, - (b, t) => b.Append(t.StorageStrategy.Value.EqualsInvocation("other"))) - .AppendLine(';') - .DetentCore(); - }) - .Append(b => - { - if(target is - { - Signature.Nature: TypeNature.PureValueType or TypeNature.ImpureValueType, - Settings.EqualityOperatorsSetting: EqualityOperatorsSetting.EmitOperatorsIfValueType or EqualityOperatorsSetting.EmitOperators - } or - { - Signature.Nature: TypeNature.ReferenceType or TypeNature.UnknownType, - Settings.EqualityOperatorsSetting: EqualityOperatorsSetting.EmitOperators - }) - { - b.Append("public static System.Boolean operator ==(") - .Append(target.Signature.Names.GenericName).Append(" a, ") - .Append(target.Signature.Names.GenericName).AppendLine(" b) => a.Equals(b);") - .Append("public static System.Boolean operator !=(") - .Append(target.Signature.Names.GenericName).Append(" a, ") - .Append(target.Signature.Names.GenericName).AppendCore(" b) => !a.Equals(b);"); - } - }) - .CloseBlockCore(); - } - public void GetHashCode(IndentedStringBuilder builder) - { - if(!target.IsEqualsRequired) - return; - - builder.OpenRegionBlock("GetHashCode") - .Comment.InheritDoc() - .AppendLine("public override System.Int32 GetHashCode() => ") - .TagSwitchExpr( - target, - (b, t) => b.Append(t.StorageStrategy.Value.GetHashCodeInvocation())) - .Append(';') - .CloseBlockCore(); - } - public void ToString(IndentedStringBuilder builder) - { - if(!target.IsToStringRequired) - return; - - builder.OpenRegionBlock("ToString") - .Comment.InheritDoc() - .Append(b => - { - if(target.Settings.ToStringSetting == ToStringSetting.Simple) - { - builder.Append("public override System.String ToString() =>") - .Append(simpleToStringExpression) - .AppendCore(';'); - } else - { - builder.Append("public override System.String ToString()") - .OpenBracesBlock() - .Append("var stringRepresentation = ").Append(simpleToStringExpression) - .AppendLine(';') - .Append("var result = $\"").Append(target.Signature.Names.GenericName) - .Append('(') - .Append(b => - { - if(target.RepresentableTypes.Count == 1) - { - b.Append('<').Append(target.RepresentableTypes[0].Signature.Names.GenericName).AppendCore('>'); - } else - { - _ = b.AppendJoin( - '|', - target.RepresentableTypes, - (b, t) => b.Append("{(") - .Append(target.Settings.TagFieldName).Append(" == ") - .Append(target.Settings.TagTypeName).Append('.').Append(t.Alias) - .Append(" ? \"<").Append(t.Alias).Append(">\" : \"").Append(t.Alias) - .AppendCore("\")}")); - } - }) - .AppendLine("){{{stringRepresentation}}}\";") - .AppendLine("return result;") - .CloseBlockCore(); - } - }) - .CloseBlockCore(); - - void simpleToStringExpression(IndentedStringBuilder b) - { - if(target.RepresentableTypes.Count > 1) - { - _ = b.TagSwitchExpr( - target, - (b, t) => b.AppendCore(t.StorageStrategy.Value.ToStringInvocation())); - } else - { - b.AppendCore(target.RepresentableTypes[0].StorageStrategy.Value.ToStringInvocation()); - } - } - } - public void IsAsFunctions(IndentedStringBuilder builder) - { - builder.OpenRegionBlock("Is/As Functions") - .AppendJoin( - StringOrChar.Empty, - target.RepresentableTypes, - (b, t) => - { - b.Comment.OpenSummary() - .Append("Determines whether this instance is representing a value of type ") - .Comment.Ref(t.Signature).Append('.') - .CloseBlock() - .Comment.OpenReturns() - .Comment.Langword("true").Append(" if this instance is representing a value of type ") - .Comment.Ref(t.Signature).Append("; otherwise, ") - .Comment.Langword("false").Append('.') - .CloseBlock() - .Comment.OpenParam("value") - .Append("If this instance is representing a value of type ").Comment.Ref(t.Signature) - .Append(", this parameter will contain that value; otherwise, ").Comment.Langword("default").Append('.') - .CloseBlock() - .Append("public System.Boolean TryAs").Append(t.Alias).Append('(') - .Append(b => - { - if(t.Signature.Nature is not TypeNature.ReferenceType) - return; - b.AppendCore("[System.Diagnostics.CodeAnalysis.NotNullWhen(true)]"); - }) - .Append(" out ").Append(t.Signature.Names.FullGenericNullableName).Append(" value)") - .OpenBracesBlock() - .Append(b => - { - if(target.RepresentableTypes.Count == 1) - { - b.Append("value = ").Append(target.RepresentableTypes[0].StorageStrategy.Value.StrongInstanceVariableExpression("this")).AppendLine(';') - .AppendCore("return true;"); - } else - { - b.Append("if(") - .Append("this").Append('.').Append(target.Settings.TagFieldName) - .Append(" == ") - .Append(target.Settings.TagTypeName).Append('.').Append(t.Alias).Append(')') - .OpenBracesBlock() - .Append("value = ").Append(t.StorageStrategy.Value.StrongInstanceVariableExpression("this")).AppendLine(';') - .Append("return true;") - .CloseBlock() - .AppendLine("value = default;") - .AppendCore("return false;"); - } - }) - .CloseBlockCore(); - }) - .Comment.OpenSummary() - .Append("Determines whether this instance is representing a value of type ") - .Comment.TypeParamRef(target.Settings.GenericTValueName).Append('.') - .CloseBlock() - .Comment.OpenTypeParam(target.Settings.GenericTValueName) - .Append("The type whose representation in this instance to determine.") - .CloseBlock() - .Comment.OpenReturns() - .Comment.Langword("true").Append(" if this instance is representing a value of type ") - .Comment.TypeParamRef(target.Settings.GenericTValueName).Append("; otherwise, ") - .Comment.Langword("false").Append('.') - .CloseBlock() - .Append("public System.Boolean Is<").Append(target.Settings.GenericTValueName).Append(">() =>") - .Append(b => - { - _ = target.RepresentableTypes.Count > 1 - ? b.Append("typeof(").Append(target.Settings.GenericTValueName).Append(") ==") - .TagSwitchExpr( - target, - (b, a) => b.Typeof(a.Signature)) - : b.Append("typeof(").Append(target.Settings.GenericTValueName).Append(") == ") - .Typeof(target.RepresentableTypes[0].Signature); - }) - .AppendLine(';') - .Comment.OpenSummary() - .Append("Determines whether this instance is representing a value of type ") - .Comment.TypeParamRef(target.Settings.GenericTValueName).Append('.') - .CloseBlock() - .Comment.OpenParam("value") - .Append("If this instance is representing a value of type ").Comment.TypeParamRef(target.Settings.GenericTValueName) - .Append(", this parameter will contain that value; otherwise, ").Comment.Langword("default").Append('.') - .CloseBlock() - .Comment.OpenTypeParam(target.Settings.GenericTValueName) - .Append("The type whose representation in this instance to determine.") - .CloseBlock() - .Comment.OpenReturns() - .Comment.Langword("true").Append(" if this instance is representing a value of type ") - .Comment.TypeParamRef(target.Settings.GenericTValueName) - .Append("; otherwise, ").Comment.Langword("false").Append('.') - .CloseBlock() - .Append("public System.Boolean Is<").Append(target.Settings.GenericTValueName).Append(">(out ") - .Append(target.Settings.GenericTValueName).Append("? value)") - .OpenBracesBlock() - .Append(b => - { - if(target.RepresentableTypes.Count > 1) - { - _ = b.MetadataNameSwitchStmt( - target, - b => b.Append("typeof(").Append(target.Settings.GenericTValueName).Append(')'), - (b, a) => b.Append("value = ").Append(a.StorageStrategy.Value.ConvertedInstanceVariableExpression(target.Settings.GenericTValueName)) - .AppendLine(';').Append("return true;"), - b => b.AppendLine("value = default;").Append("return false;")); - } else - { - b.Append("if(typeof(").Append(target.Settings.GenericTValueName).Append(") == ") - .Typeof(target.RepresentableTypes[0].Signature).Append(")") - .OpenBracesBlock() - .Append("value = ").Append( - target.RepresentableTypes[0].StorageStrategy.Value.ConvertedInstanceVariableExpression(target.Settings.GenericTValueName)) - .AppendLine(';') - .Append("return true;") - .CloseBlock() - .Append("else") - .OpenBracesBlock() - .AppendLine("value = default;") - .Append("return false;") - .CloseBlockCore(); - } - }) - .CloseBlock() - .Comment.OpenSummary() - .Append("Determines whether this instance is representing an instance of ") - .Comment.ParamRef("type").Append('.') - .CloseBlock() - .Comment.OpenParam("type") - .Append("The type whose representation in this instance to determine.") - .CloseBlock() - .Comment.OpenReturns() - .Comment.Langword("true").Append(" if this instance is representing an instance of ") - .Comment.ParamRef("type").Append("; otherwise, ").Comment.Langword("false").Append('.') - .CloseBlock() - .AppendLine("public System.Boolean Is(System.Type type) =>") - .Append(b => - { - _ = target.RepresentableTypes.Count > 0 - ? b.Append("type == ").TagSwitchExpr( - target, - (b, a) => b.Typeof(a.Signature)) - : b.Append("type == ").Typeof(target.RepresentableTypes[0].Signature); - }) - .AppendLine(';') - .Comment.OpenSummary() - .Append("Retrieves the value represented by this instance as an instance of ") - .Comment.TypeParamRef(target.Settings.GenericTValueName).Append('.') - .CloseBlock() - .Comment.OpenTypeParam(target.Settings.GenericTValueName) - .Append("The type to retrieve the represented value as.") - .CloseBlock() - .Comment.OpenReturns() - .Append("The currently represented value as an instance of ").Comment.TypeParamRef(target.Settings.GenericTValueName).Append('.') - .CloseBlock() - .Append("public ").Append(target.Settings.GenericTValueName) - .Append(" As<").Append(target.Settings.GenericTValueName).AppendLine(">() =>") - .Append(b => - { - if(target.RepresentableTypes.Count > 1) - { - b.TagSwitchExpr( - target, - (b, a) => b.Append("typeof(").Append(target.Settings.GenericTValueName).Append(") == ") - .Typeof(a.Signature).AppendLine() - .Append("? ").AppendLine(a.StorageStrategy.Value.ConvertedInstanceVariableExpression(target.Settings.GenericTValueName)) - .Append(": ") - .InvalidConversionThrow( - fromTypeNameExpression: $"typeof({target.Signature.Names.FullGenericName})", - representedTypeNameExpression: "this.RepresentedType", - toTypeNameExpression: $"typeof({target.Settings.GenericTValueName})")) - .AppendCore(';'); - } else - { - b.Append("typeof(").Append(target.Settings.GenericTValueName).Append(") == ") - .Typeof(target.RepresentableTypes[0].Signature).AppendLine() - .Append("? ").Append( - target.RepresentableTypes[0].StorageStrategy.Value.ConvertedInstanceVariableExpression(target.Settings.GenericTValueName)) - .AppendLine() - .Append(": ") - .InvalidConversionThrow( - fromTypeNameExpression: $"typeof({target.Signature.Names.FullGenericName})", - representedTypeNameExpression: "this.RepresentedType", - toTypeNameExpression: $"typeof({target.Settings.GenericTValueName})") - .AppendCore(';'); - } - }) - .CloseBlockCore(); - } - public void IsAsProperties(IndentedStringBuilder builder) - { - _ = builder.OpenRegionBlock("Is/As Properties"); - if(target.RepresentableTypes.Count > 1) - { - builder.AppendJoinLines( - StringOrChar.Semicolon, - target.RepresentableTypes, - (b, a) => - b.Comment.OpenSummary() - .Append("Gets a value indicating whether this instance is representing a value of type ").Comment.Ref(a.Signature).Append('.') - .CloseBlock() - .Append(b => - { - if(a.Signature.Nature is TypeNature.ReferenceType) - { - b.Append("[global::System.Diagnostics.CodeAnalysis.MemberNotNullWhen(true, \"As").Append(a.Alias).Append("\")]").AppendLineCore(); - } - - if(target.RepresentableTypes.Count == 2) - { - var otherRepresentable = target.RepresentableTypes.Single(r => r.Alias != a.Alias); - - if(otherRepresentable.Signature.Nature == TypeNature.ReferenceType) - { - b.Append("[global::System.Diagnostics.CodeAnalysis.MemberNotNullWhen(false, \"As").Append(otherRepresentable.Alias).Append("\")]").AppendLineCore(); - } - } - }) - .Append("public System.Boolean Is").Append(a.Alias).Append(" => ") - .Append(target.Settings.TagFieldName).Append(" == ") - .Append(target.Settings.TagTypeName).Append('.').Append(a.Alias)) - .AppendLine(';') - .AppendJoinLines( - StringOrChar.Semicolon, - target.RepresentableTypes, - (b, a) => - b.Comment.OpenSummary() - .Append("Retrieves the value represented by this instance as a ").Comment.Ref(a.Signature).Append('.') - .CloseBlock() - .Append("public ").Append(a.Signature.Names.FullGenericName) - .Append(a.Signature.Nature == TypeNature.ReferenceType ? "?" : String.Empty) - .Append(" As").Append(a.Alias).Append(" => ") - .Append(target.Settings.TagFieldName).Append(" == ") - .Append(target.Settings.TagTypeName).Append('.').AppendLine(a.Alias) - .Append("? ").AppendLine(a.StorageStrategy.Value.StrongInstanceVariableExpression("this")) - .Append(": ").Append(a.Signature.Nature == TypeNature.ReferenceType ? "null" : "default")) - .AppendCore(';'); - } else - { - var attribute = target.RepresentableTypes[0]; - builder.Comment.OpenSummary() - .Append("Gets a value indicating whether this instance is representing a value of type ").Comment.Ref(attribute.Signature).Append('.') - .CloseBlock() - .Append("public System.Boolean Is").Append(attribute.Alias).AppendLine(" => true;") - .Comment.OpenSummary() - .Append("Retrieve the value represented by this instance as a ").Comment.Ref(attribute.Signature).Append('.') - .CloseBlock() - .Append("public ").Append(attribute.Signature.Names.FullGenericName).Append(" As").Append(attribute.Alias).Append(" => ") - .Append(attribute.StorageStrategy.Value.StrongInstanceVariableExpression("this")) - .AppendCore(';'); - } - - builder.CloseBlockCore(); - } - public void RepresentedTypes(IndentedStringBuilder builder) - { - builder.OpenRegionBlock("Represented Type") - .Comment.OpenSummary() - .Append("Gets the types of value this union type can represent.") - .CloseBlock() - .AppendLine("public static System.Collections.Generic.IReadOnlyCollection RepresentableTypes { get; } = ") - .Indent() - .Append(target.ScopedDataTypeName).Append(".RepresentableTypes;") - .Detent() - .Comment.OpenSummary() - .Append("Gets the type of value represented by this instance.") - .CloseBlock() - .AppendLine("public System.Type RepresentedType => ") - .TagSwitchExpr( - target, - (b, a) => b.Typeof(a.Signature)) - .Append(';') - .CloseBlockCore(); - } - public void Match(IndentedStringBuilder builder) - { - builder.OpenRegionBlock("Match") - .Comment.OpenSummary() - .Append("Invokes a projection based on the type of value being represented.") - .CloseBlockCore(); - - foreach(var attribute in target.RepresentableTypes) - { - builder.Comment.OpenParam($"on{attribute.Alias}") - .Append("The projection to invoke if the union is currently representing an instance of ").Comment.Ref(attribute.Signature).Append('.') - .CloseBlockCore(); - } - - builder.Comment.OpenTypeParam(target.Settings.MatchTypeName) - .Append("The type of value produced by the projections passed.") - .CloseBlock() - .Comment.OpenReturns() - .Append("The projected value.") - .CloseBlock() - .Append("public ").Append(target.Settings.MatchTypeName).Append(" Match<").Append(target.Settings.MatchTypeName).AppendLine(">(") - .Indent() - .AppendJoinLines(target.RepresentableTypes - .Select>(a => b => - b.Append("System.Func<").Append(a.Signature.Names.FullGenericNullableName).Append(", ") - .Append(target.Settings.MatchTypeName).Append("> on").Append(a.Alias))) - .AppendLine(") =>") - .Detent() - .TagSwitchExpr( - target, - (b, t) => - b.Append("on").Append(t.Alias).Append(".Invoke(") - .Append(t.StorageStrategy.Value.StrongInstanceVariableExpression("this")) - .AppendLine(")")) - .Append(';') - .CloseBlockCore(); - } - public void Switch(IndentedStringBuilder builder) - { - builder.OpenRegionBlock("Switch") - .Comment.OpenSummary() - .Append("Invokes a handler based on the type of value being represented.") - .CloseBlockCore(); - - foreach(var attribute in target.RepresentableTypes) - { - builder.Comment.OpenParam($"on{attribute.Alias}") - .Append("The handler to invoke if the union is currently representing an instance of ").Comment.Ref(attribute.Signature).Append('.') - .CloseBlockCore(); - } - - builder.AppendLine("public void Switch(") - .Indent() - .AppendJoinLines(target.RepresentableTypes - .Select>(a => b => - b.Append("System.Action<").Append(a.Signature.Names.FullGenericNullableName).Append("> on").Append(a.Alias))) - .AppendLine(")") - .Detent() - .OpenBracesBlock() - .TagSwitchStmt( - target, - (b, t) => - b.Append("on").Append(t.Alias).Append(".Invoke(") - .Append(t.StorageStrategy.Value.StrongInstanceVariableExpression("this")) - .AppendLine(");") - .Append("return;")) - .CloseBlock() - .CloseBlockCore(); - } - public void ScopedData(IndentedStringBuilder builder) - { - builder.OpenRegionBlock("Scoped Data") - .Append("file static class ").Append(target.ScopedDataTypeName) - .OpenBracesBlock() - .AppendLine("public static System.Collections.Concurrent.ConcurrentDictionary Cache { get; } = new();") - .AppendLine("public static System.Collections.Generic.HashSet RepresentableTypes { get; } = ") - .AppendLine("new ()") - .OpenBracesBlock() - .AppendJoinLines( - target.RepresentableTypes, - (b, a) => b.Typeof(a.Signature)) - .CloseBlock() - .AppendLine(';') - .CloseBlock() - .CloseBlockCore(); - } - public void Factories(IndentedStringBuilder builder) - { - var tValueName = target.Settings.GenericTValueName; - builder.OpenRegionBlock("Factories") - .AppendJoin( - StringOrChar.Empty, - target.RepresentableTypes.Where(a => a.Factory.RequiresGeneration) - .Select>(a => b => - { - b.Comment.OpenSummary() - .Append("Creates a new instance of ").Comment.SeeRef(target) - .Append(" representing an instance of ").Comment.Ref(a.Signature).Append('.') - .CloseBlock() - .Comment.OpenParam("value") - .Append("The value to be represented by the new instance of ").Comment.SeeRef(target).Append('.') - .CloseBlock() - .Comment.OpenReturns() - .Append("A new instance of ").Comment.SeeRef(target).Append(" representing ").Comment.ParamRef("value").Append('.') - .CloseBlock() - .Append("public static ").Append(target.Signature.Names.GenericName).Append(' ') - .Append(a.Factory.Name).Append("([RhoMicro.CodeAnalysis.UnionTypeFactory]").Append(a.Signature.Names.FullGenericNullableName) - .Append(" value) => new(value);").AppendLineCore(); - })) - .Comment.OpenSummary() - .Append("Attempts to create an instance of ").Comment.SeeRef(target) - .Append(" from an instance of ").Comment.TypeParamRef(tValueName).Append('.') - .CloseBlock() - .Comment.OpenParam("value") - .Append("The value from which to attempt to create an instance of ").Comment.SeeRef(target).Append('.') - .CloseBlock() - .Comment.OpenParam("result") - .Append("If an instance of ").Comment.SeeRef(target) - .Append(" could successfully be created, this parameter will contain the newly created instance; otherwise, ") - .Comment.Langword("default").Append('.') - .CloseBlock() - .Comment.OpenReturns() - .Comment.Langword("true").Append(" if an instance of ").Comment.SeeRef(target).Append(" could successfully be created; otherwise, ") - .Comment.Langword("false").AppendLine('.') - .CloseBlock() - .Append("public static System.Boolean TryCreate<").Append(tValueName) - .Append(">(").Append(tValueName).Append(" value, ").Append(b => - { - if(target.Signature.Nature == TypeNature.ReferenceType) - { - b.AppendCore("[System.Diagnostics.CodeAnalysis.NotNullWhen(true)]"); - } - }).Append(" out ") - .Append(target.Signature.Names.GenericName).Append(b => - { - if(target.Signature.Nature == TypeNature.ReferenceType) - { - b.AppendCore('?'); - } - }).Append(" result)") - .OpenBracesBlock() - .MetadataNameSwitchStmt( - target: target, - typeExpr: b => b.Append("typeof(").Append(tValueName).Append(")"), - caseBody: (b, t) => _ = b.Operators + - "result = " + t.Factory.Name + '(' + UtilUnsafeConvert(tValueName, t.Signature, "value") + ");" + NewLine + - "return true;", - defaultBody: b => b.Append(TryCreateDefaultCase)) - .CloseBlock() - .Comment.OpenSummary() - .Append("Creates an instance of ").Comment.SeeRef(target).Append(" from an instance of ").Comment.TypeParamRef(tValueName).Append('.') - .CloseBlock() - .Comment.OpenParam("value") - .Append("The value from which to create an instance of ").Comment.SeeRef(target).Append('.') - .CloseBlock() - .Comment.OpenReturns() - .Append("A new instance of ").Comment.SeeRef(target).Append(" representing ").Comment.ParamRef("value").Append('.') - .CloseBlock() - .Append("public static ").Append(target.Signature.Names.GenericName) - .Append(" Create<").Append(tValueName).Append(">(").Append(tValueName).Append(" value)") - .OpenBracesBlock() - .MetadataNameSwitchStmt( - target: target, - typeExpr: b => b.Append("typeof(").Append(tValueName).Append(")"), - caseBody: (b, t) => _ = b.Operators + - "return " + t.Factory.Name + '(' + UtilUnsafeConvert(tValueName, t.Signature, "value") + ");", - defaultBody: b => b.Append(CreateDefaultCase)) - .CloseBlock() - .CloseBlockCore(); - } - public void CreateDefaultCase(IndentedStringBuilder builder) - { - builder.Append("var sourceType = typeof(").Append(target.Settings.GenericTValueName).AppendLine(");") - .Append("if(!").Append(target.ScopedDataTypeName).Append(".Cache.TryGetValue(sourceType, out var weakMatch))") - .OpenBracesBlock() - .Append("if(!").Append(b => b.UtilIsMarked("sourceType")).Append(')') - .OpenBracesBlock() - .Append(invalidCreationThrow) - .CloseBlock() - .Append(ConversionCacheWeakMatchExpr) - .CloseBlock() - .Append("var match = (System.Func)weakMatch;") - .AppendLine("var matchResult = match.Invoke(value);") - .AppendLine("if(!matchResult.Item1)") - .OpenBracesBlock() - .Append(invalidCreationThrow) - .CloseBlock() - .Append("return matchResult.Item2;") - .AppendLineCore(); - - void invalidCreationThrow(IndentedStringBuilder builder) => - builder.Append("throw new System.InvalidOperationException($\"Unable to create an instance of ") - .Append(target.Signature.Names.FullGenericName) - .Append(" from an instance of {typeof(").Append(target.Settings.GenericTValueName).AppendCore(")}.\");"); - } - public void ConversionCacheWeakMatchExpr(IndentedStringBuilder builder) => - builder.Append("weakMatch = ").Append(target.ScopedDataTypeName).Append(".Cache.GetOrAdd(sourceType, t =>") - .OpenBracesBlock() - .Append("var tupleType = typeof(System.ValueTuple);") - .AppendLine("var matchMethod = sourceType.GetMethod(nameof(Match), System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public)") - .Indent() - .AppendLine("?.MakeGenericMethod(tupleType) ??") - .AppendLine("throw new System.InvalidOperationException(\"Unable to locate match function on source union type. This indicates a bug in the marker detection algorithm.\");") - .Detent() - .Append("var targetFactoryMap = ").Typeof(target.Signature).AppendLine(".GetMethods()") - .Indent() - .Append(".Where(c => c.CustomAttributes.Any(a => a.AttributeType.FullName == \"").Append(UnionTypeFactoryAttribute.MetadataName).AppendLine("\"))") - .AppendLine(".ToDictionary(c => c.GetParameters()[0].ParameterType);") - .Detent() - .AppendLine("var handlers = matchMethod.GetParameters()") - .Indent() - .AppendLine(".Select(p => p.ParameterType.GenericTypeArguments[0])") - .AppendLine(".Select(t => (ParameterExpr: System.Linq.Expressions.Expression.Parameter(t), ParameterExprType: t))") - .Append(".Select(t =>") - .OpenBracesBlock() - .AppendLine("var delegateType = typeof(System.Func<,>).MakeGenericType(t.ParameterExprType, tupleType);") - .AppendLine("System.Linq.Expressions.Expression expression = targetFactoryMap.TryGetValue(t.ParameterExprType, out var factory)") - .Indent() - .AppendLine("? System.Linq.Expressions.Expression.New(tupleType.GetConstructors()[0], System.Linq.Expressions.Expression.Constant(true), System.Linq.Expressions.Expression.Call(factory, t.ParameterExpr))") - .AppendLine(": System.Linq.Expressions.Expression.Default(tupleType);") - .Detent() - .AppendLine("return System.Linq.Expressions.Expression.Lambda(delegateType, expression, t.ParameterExpr);") - .CloseBlock() - .Append(");") - .Detent() - .AppendLine("var paramExpr = System.Linq.Expressions.Expression.Parameter(sourceType);") - .AppendLine("var callExpr = System.Linq.Expressions.Expression.Call(paramExpr, matchMethod, handlers);") - .AppendLine("var lambdaExpr = System.Linq.Expressions.Expression.Lambda(callExpr, paramExpr);") - .AppendLine("var result = lambdaExpr.Compile();") - .AppendLine("return result;") - .CloseBlock() - .Append(");"); - public void TryCreateDefaultCase(IndentedStringBuilder builder) - { - builder.Append("var sourceType = typeof(").Append(target.Settings.GenericTValueName).AppendLine(");") - .Append("if(!").Append(target.ScopedDataTypeName).AppendLine(".Cache.TryGetValue(sourceType, out var weakMatch))") - .OpenBracesBlock() - .Append("if(!RhoMicro.CodeAnalysis.UnionsGenerator.Generated.Util.IsMarked(sourceType))") - .OpenBracesBlock() - .AppendLine("result = default;") - .Append("return false;") - .CloseBlock() - .Append(ConversionCacheWeakMatchExpr) - .CloseBlock() - .Append("var match = (System.Func)weakMatch;") - .AppendLine("var matchResult = match.Invoke(value);") - .AppendLine("if(!matchResult.Item1)") - .OpenBracesBlock() - .AppendLine("result = default;") - .Append("return false;") - .CloseBlock() - .AppendLine("result = matchResult.Item2;") - .Append("return true;") - .AppendLineCore(); - } - public void Fields(IndentedStringBuilder builder) - { - builder.OpenRegionBlock("Fields") - .Append(target.StrategyHostContainer.Value.ReferenceTypeContainerField) - .Append(target.StrategyHostContainer.Value.DedicatedReferenceFields) - .Append(target.StrategyHostContainer.Value.DedicatedImpureAndUnknownFields) - .Append(target.StrategyHostContainer.Value.DedicatedPureValueTypeFields) - .AppendCore(target.StrategyHostContainer.Value.ValueTypeContainerField); - - if(target.RepresentableTypes.Count > 1) - { - builder.Comment.OpenSummary() - .Append("Used to determine the currently represented type and value.") - .CloseBlock() - .GeneratedUnnavigableInternalCode(target) - .Append("private readonly ") - .Append(target.Settings.TagTypeName) - .Append(' ') - .Append(target.Settings.TagFieldName) - .AppendCore(';'); - } - - builder.CloseBlockCore(); - } - public void Constructors(IndentedStringBuilder builder) - { - var ctors = target.RepresentableTypes.Select(t => - new IndentedStringBuilderAppendable(b => - { - var accessibility = target.GetSpecificAccessibility(t.Signature); - b.Comment.OpenSummary() - .Append("Creates a new instance of ").Comment.SeeRef(target).Append("representing an instance of ").Comment.Ref(t.Signature).Append('.') - .CloseBlockCore(); - - if(!t.Factory.RequiresGeneration) - { - b.Comment.OpenRemarks() - .Append("Using this constructor will sidestep any validation or sideeffects defined in the ").Comment.SeeRef(t.Factory.Name).Append(" factory method.") - .CloseBlockCore(); - } - - b.AppendLine(ConstantSources.EditorBrowsableNever) - .AppendLine(ConstantSources.GeneratedCode) - .Append(accessibility).Append(' ').Append(target.Signature.Names.Name) - .Append('(').Append(t.Signature.Names.FullGenericNullableName).Append(" value)") - .OpenBlockCore(Blocks.Braces(b.Options.NewLine)); - - if(target.RepresentableTypes.Count > 1) - { - b.Append(target.Settings.TagFieldName).Append(" = ") - .Append(target.Settings.TagTypeName).Append('.').Append(t.Alias).Append(';') - .AppendLineCore(); - } - - b.Append(t.StorageStrategy.Value - .InstanceVariableAssignmentExpression("value", "this")).Append(';') - .CloseBlockCore(); - })); - - builder.OpenRegionBlock("Constructors") - .AppendJoinLines(StringOrChar.Empty, ctors) - .CloseBlockCore(); - } - public void TagType(IndentedStringBuilder builder) - { - var (representableTypes, settings) = (target.RepresentableTypes, target.Settings); - - if(representableTypes.Count < 2) - return; - - builder - .OpenRegionBlock("Tag Type") - .Comment.OpenSummary() - .Append("Defines tags to discriminate between representable types.") - .CloseBlock() - .GeneratedUnnavigableInternalCode(target) - .Append("private enum ").Append(settings.TagTypeName).Append(" : System.Byte") - .OpenBracesBlock() - .Comment.OpenSummary() - .Append("Used when not representing any type due to e.g. incorrect or missing initialization.") - .CloseBlock() - .Append(target.Settings.TagNoneName) - .AppendLine(',') - .AppendLine() - .AppendJoinLines(representableTypes, (b, t) => - b.Comment.OpenSummary() - .Append("Used when representing an instance of ").Comment.Ref(t.Signature).Append('.') - .CloseBlock() - .Append(t.Alias)) - .CloseBlock() - .CloseBlockCore(); - } - public void JsonConverterType(IndentedStringBuilder builder) - { - var (representableTypes, settings) = (target.RepresentableTypes, target.Settings); - - if(!settings.Miscellaneous.HasFlag(MiscellaneousSettings.GenerateJsonConverter)) - return; - - using var __ = builder.OpenRegionBlockScope("Json Converter Type"); - - builder.Comment.OpenSummary() - .Append("Implements json conversion logic for the ").Comment.Ref(target.Signature).AppendLine(" type.") - .CloseBlock() - .AppendLine(ConstantSources.GeneratedCode) - .Append("public sealed class JsonConverter : System.Text.Json.Serialization.JsonConverter<") - .Append(target.Signature.Names.GenericName).AppendLine('>') - .OpenBracesBlock() - .Append("sealed class Dto") - .OpenBracesBlock() - .Append("public static Dto Create(").Append(target.Signature.Names.GenericName) - .Append(" value) => new()") - .OpenBracesBlock() - .AppendCore("RepresentedType = "); - - if(target.RepresentableTypes.Count == 1) - { - builder.Append('"').Append(target.RepresentableTypes[0].Signature.Names.FullMetadataName).AppendCore('"'); - } else - { - _ = builder.TagSwitchExpr( - target, - (b, t) => b.Append('"').Append(t.Signature.Names.FullMetadataName).AppendCore('"'), - instanceExpr: "value"); - } - - builder.AppendLine(',').AppendCore("RepresentedValue = "); - - if(target.RepresentableTypes.Count == 1) - { - builder.AppendCore( - target.RepresentableTypes[0].StorageStrategy.Value - .InstanceVariableExpression("value")); - } else - { - _ = builder.TagSwitchExpr( - target, - (b, t) => b.AppendCore( - t.StorageStrategy.Value.InstanceVariableExpression(instance: "value")), - instanceExpr: "value"); - } - - builder.CloseBlock() - .AppendLine(';') - .Append("public ").Append(target.Signature.Names.GenericName).AppendLine(" Reconstitute() =>") - .FullMetadataNameSwitchExpr( - target, - "RepresentedType", - (b, t) => - { - var isNullable = t.Options.HasFlag(UnionTypeOptions.Nullable) || t.Signature.Names.FullOpenGenericName == "System.Nullable<>"; - if(isNullable) - { - b.Append(target.Signature.Names.GenericName).Append('.').Append(t.Factory.Name).AppendLine('(') - .Indent() - .AppendLine("RepresentedValue != null") - .Append("? System.Text.Json.JsonSerializer.Deserialize<").Append(t.Signature.Names.FullGenericName).AppendLine(">((System.Text.Json.JsonElement)RepresentedValue)") - .Indent() - .Append("?? throw new System.Text.Json.JsonException(\"Unable to deserialize an instance of the nullable type ") - .Append(t.Signature.Names.FullGenericName) - .Append(" from an unknown value in the RepresentedValue property.\")") - .Detent() - .Append(": null)") - .DetentCore(); - } else - { - b.Append(target.Signature.Names.GenericName).Append('.').Append(t.Factory.Name).AppendLine('(') - .Indent() - .Append("System.Text.Json.JsonSerializer.Deserialize<") - .Append(t.Signature.Names.FullGenericName) - .AppendLine(">(") - .Indent() - .AppendLine("(System.Text.Json.JsonElement)") - .AppendLine("(RepresentedValue") - .Append("?? throw new System.Text.Json.JsonException(\"Unable to deserialize an instance of the non-nullable type ") - .Append(t.Signature.Names.FullGenericName) - .AppendLine(" from a null value in the RepresentedValue property.\")))") - .Detent() - .Append(b => - { - if(t.Signature.Nature is TypeNature.ReferenceType or TypeNature.UnknownType) - { - b.Append("?? throw new System.Text.Json.JsonException(\"Unable to deserialize an instance of the non-nullable type ") - .Append(t.Signature.Names.FullGenericName) - .AppendCore(" from an unknown value in the RepresentedValue property.\")"); - } - }) - .Append(")") - .DetentCore(); - } - }, defaultCase: b => b - .Append("throw new System.Text.Json.JsonException($\"Unable to deserialize a union instance representing an instance of {RepresentedType} as an instance of ") - .Append(target.Signature.Names.FullGenericName).AppendCore("\")")) - .AppendLine(';') - .AppendLine("public System.String RepresentedType { get; set; }") - .AppendLine("public System.Object? RepresentedValue { get; set; }") - .CloseBlock() - .Comment.InheritDoc() - .Append("public override ").Append(target.Signature.Names.GenericName) - .Append(target.Signature.Nature == TypeNature.ReferenceType ? "?" : String.Empty) - .AppendLine(" Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options)") - .OpenBracesBlock() - .AppendLine("var dto = System.Text.Json.JsonSerializer.Deserialize(ref reader, options);") - .AppendLine("if(dto == null)") - .OpenBracesBlock() - .Append("throw new System.Text.Json.JsonException(\"Unable to deserialize union instance from invalid dto.\");") - .CloseBlock() - .AppendLine("var result = dto.Reconstitute();") - .AppendLine("return result;") - .CloseBlock() - .Comment.InheritDoc() - .Append("public override void Write(System.Text.Json.Utf8JsonWriter writer, ") - .Append(target.Signature.Names.GenericName) - .AppendLine(" value, System.Text.Json.JsonSerializerOptions options) => System.Text.Json.JsonSerializer.Serialize(writer, Dto.Create(value), options);") - .CloseBlockCore(); - } - public void ValueTypeContainerType(IndentedStringBuilder builder) - { - using var _ = builder.OpenRegionBlockScope("Value Type Container"); - target.StrategyHostContainer.Value.ValueTypeContainerType(builder); - } - public void NestedTypes(IndentedStringBuilder builder) - { - using var _ = builder.OpenRegionBlockScope("Nested Types"); - - ValueTypeContainerType(builder); - TagType(builder); - JsonConverterType(builder); - } -} diff --git a/UnionsGenerator/Transformation/Visitors/IVisitor.cs b/UnionsGenerator/Transformation/Visitors/IVisitor.cs deleted file mode 100644 index 9adede0..0000000 --- a/UnionsGenerator/Transformation/Visitors/IVisitor.cs +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Visitors; - -using RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -/// -/// Represents a visitor to visit models. -/// -/// The type of model to visit. -public interface IVisitor -{ - /// - /// Visits a model. - /// - /// - /// Implementations should never call s own - /// function, - /// as that will likely induce a . - /// - /// The model to visit. - void Visit(TModel model); -} diff --git a/UnionsGenerator/Transformation/Visitors/SourceTextVisitor.cs b/UnionsGenerator/Transformation/Visitors/SourceTextVisitor.cs deleted file mode 100644 index 1315f41..0000000 --- a/UnionsGenerator/Transformation/Visitors/SourceTextVisitor.cs +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Visitors; - -using RhoMicro.CodeAnalysis.Library.Text; -using RhoMicro.CodeAnalysis.UnionsGenerator.Models; - -internal sealed class SourceTextVisitor(IndentedStringBuilder builder) : IVisitor -{ - private readonly IndentedStringBuilder _builder = builder; - - public void Visit(UnionTypeModel model) => _builder.Append(new AppendableSourceText(model)); - - public override String ToString() => _builder.ToString(); -} diff --git a/UnionsGenerator/Transformation/Visitors/StructuralRepresentationVisitor.cs b/UnionsGenerator/Transformation/Visitors/StructuralRepresentationVisitor.cs deleted file mode 100644 index 21e9021..0000000 --- a/UnionsGenerator/Transformation/Visitors/StructuralRepresentationVisitor.cs +++ /dev/null @@ -1,228 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Transformation.Visitors -{ - using RhoMicro.CodeAnalysis; - using RhoMicro.CodeAnalysis.Library.Text; - using RhoMicro.CodeAnalysis.UnionsGenerator.Models; - - internal sealed class StructuralRepresentationVisitor(IndentedStringBuilder builder) : IVisitor - { - private readonly IndentedStringBuilder _builder = builder; - - public static StructuralRepresentationVisitor Create() => new(new()); - public static StructuralRepresentationVisitor Create(IndentedStringBuilderOptions builderOptions) => new(new(builderOptions)); - - public void Visit(UnionTypeModel model) => _builder.ModelString(model); - - public override String ToString() => _builder.ToString(); - } -} - -namespace RhoMicro.CodeAnalysis.Library.Text -{ - using RhoMicro.CodeAnalysis.UnionsGenerator.Models; - using RhoMicro.CodeAnalysis.UnionsGenerator.Models.Storage; - - internal partial class Blocks - { - public static Block SameLineBraces = new('{', '}'); - } - - internal partial class IndentedStringBuilder - { - private IndentedStringBuilder OpenSameLineBracesBlock() => OpenBlock(Blocks.SameLineBraces); - private IndentedStringBuilder CloseSameLineBracesBlock() => CloseBlock(); - public IndentedStringBuilder ModelString(RelatedTypeModel model) => - OpenSameLineBracesBlock() - .Property(nameof(model.Signature), ModelString, model.Signature) - .Property(nameof(model.RepresentableTypeSignatures), ModelString, model.RepresentableTypeSignatures, isLast: true) - .CloseSameLineBracesBlock(); - public IndentedStringBuilder ModelString(StorageStrategy strategy) => - OpenSameLineBracesBlock() - .Property(nameof(strategy.SelectedOption), strategy.SelectedOption) - .Property(nameof(strategy.ActualOption), strategy.ActualOption) - .Property(nameof(strategy.FieldName), strategy.FieldName) - .Property(nameof(strategy.NullableFieldQuestionMark), strategy.NullableFieldQuestionMark) - .Property(nameof(strategy.Violation), strategy.Violation, isLast: true) - .CloseSameLineBracesBlock(); - public IndentedStringBuilder ModelString(RelationModel model) => - OpenSameLineBracesBlock() - .Property(nameof(model.RelatedType), ModelString, model.RelatedType) - .Property(nameof(model.RelationType), model.RelationType, isLast: true) - .CloseSameLineBracesBlock(); - public IndentedStringBuilder ModelString(TypeNamesModel model) => - OpenSameLineBracesBlock() - .Property(nameof(model.CommentRefString), model.CommentRefString) - .Property(nameof(model.ContainingTypesString), model.ContainingTypesString) - .Property(nameof(model.FullGenericName), model.FullGenericName) - .Property(nameof(model.FullGenericNullableName), model.FullGenericNullableName) - .Property(nameof(model.FullMetadataName), model.FullMetadataName) - .Property(nameof(model.FullOpenGenericName), model.FullOpenGenericName) - .Property(nameof(model.GenericName), model.GenericName) - .Property(nameof(model.IdentifierOrHintName), model.IdentifierOrHintName) - .Property(nameof(model.FullIdentifierOrHintName), model.FullIdentifierOrHintName) - .Property(nameof(model.OpenGenericName), model.OpenGenericName) - .Property(nameof(model.TypeArgsString), model.TypeArgsString) - .Property(nameof(model.Name), model.Name) - .Property(nameof(model.Namespace), model.Namespace, isLast: true) - .CloseSameLineBracesBlock(); - public IndentedStringBuilder ModelString(SettingsModel model) => - OpenSameLineBracesBlock() - #region Settings - .Property(nameof(model.ToStringSetting), model.ToStringSetting) - .Property(nameof(model.Layout), model.Layout) - .Property(nameof(model.DiagnosticsLevel), model.DiagnosticsLevel) - .Property(nameof(model.ConstructorAccessibility), model.ConstructorAccessibility) - .Property(nameof(model.InterfaceMatchSetting), model.InterfaceMatchSetting) - .Property(nameof(model.EqualityOperatorsSetting), model.EqualityOperatorsSetting) - .Property(nameof(model.Miscellaneous), model.Miscellaneous) - #endregion - #region Strings - .Property(nameof(model.TypeDeclarationPreface), model.TypeDeclarationPreface) - .Property(nameof(model.GenericTValueName), model.GenericTValueName) - .Property(nameof(model.TryConvertTypeName), model.TryConvertTypeName) - .Property(nameof(model.MatchTypeName), model.MatchTypeName) - .Property(nameof(model.TagTypeName), model.TagTypeName) - .Property(nameof(model.ValueTypeContainerTypeName), model.ValueTypeContainerTypeName) - .Property(nameof(model.ValueTypeContainerName), model.ValueTypeContainerName) - .Property(nameof(model.ReferenceTypeContainerName), model.ReferenceTypeContainerName) - .Property(nameof(model.TagFieldName), model.TagFieldName) - .Property(nameof(model.TagNoneName), model.TagNoneName) - .Property(nameof(model.JsonConverterTypeName), model.JsonConverterTypeName, isLast: true) - #endregion - .CloseSameLineBracesBlock(); - public IndentedStringBuilder ModelString(GroupsModel model) => - OpenSameLineBracesBlock() - .Property(nameof(model.Names), model.Names) - .Property(nameof(model.Groups), ModelString, model.Groups, isLast: true) - .CloseSameLineBracesBlock(); - public IndentedStringBuilder ModelString(GroupModel model) => - OpenSameLineBracesBlock() - .Property(nameof(model.Name), model.Name) - .Property(nameof(model.Members), ModelString, model.Members) - .CloseSameLineBracesBlock(); - public IndentedStringBuilder ModelString(UnionTypeModel model) => - OpenSameLineBracesBlock() - .Property(nameof(model.Groups), ModelString, model.Groups) - .Property(nameof(model.IsEqualsRequired), model.IsEqualsRequired) - .Property(nameof(model.IsGenericType), model.IsGenericType) - .Property(nameof(model.ScopedDataTypeName), model.ScopedDataTypeName) - .Property(nameof(model.Signature), ModelString, model.Signature) - .Property(nameof(model.RepresentableTypes), ModelString, model.RepresentableTypes) - .Property(nameof(model.Relations), ModelString, model.Relations) - .Property(nameof(model.Settings), ModelString, model.Settings, isLast: true) - .CloseSameLineBracesBlock(); - public IndentedStringBuilder ModelString(FactoryModel model) => - OpenSameLineBracesBlock() - .Property(nameof(model.Name), model.Name) - .Property(nameof(model.Parameter), ModelString, model.Parameter) - .Property(nameof(model.RequiresGeneration), model.RequiresGeneration, isLast: true) - .CloseSameLineBracesBlock(); - public IndentedStringBuilder ModelString(RepresentableTypeModel model) => - OpenSameLineBracesBlock() - .Property(nameof(model.Alias), model.Alias) - .Property(nameof(model.IsBaseClassToUnionType), model.IsBaseClassToUnionType) - .Property(nameof(model.OmitConversionOperators), model.OmitConversionOperators) - .Property(nameof(model.Options), model.Options) - .Property(nameof(model.Storage), model.Storage) - .Property(nameof(model.Groups), model.Groups) - .Property(nameof(model.Factory), ModelString, model.Factory) - .Property(nameof(model.StorageStrategy), ModelString, model.StorageStrategy.Value) - .Property(nameof(model.Signature), ModelString, model.Signature, isLast: true) - .CloseSameLineBracesBlock(); - public IndentedStringBuilder ModelString(TypeSignatureModel model) => - OpenSameLineBracesBlock() - .Property(nameof(model.HasNoBaseClass), model.HasNoBaseClass) - .Property(nameof(model.IsGenericType), model.IsGenericType) - .Property(nameof(model.IsInterface), model.IsInterface) - .Property(nameof(model.IsNullableAnnotated), model.IsNullableAnnotated) - .Property(nameof(model.IsRecord), model.IsRecord) - .Property(nameof(model.IsStatic), model.IsStatic) - .Property(nameof(model.IsTypeParameter), model.IsTypeParameter) - .Property(nameof(model.TypeArgs), model.TypeArgs.Select(a => a.Names.FullGenericNullableName)) - .Property(nameof(model.DeclarationKeyword), model.DeclarationKeyword) - .Property(nameof(model.Nature), model.Nature) - .Property(nameof(model.Names), ModelString, model.Names, isLast: true) - .CloseSameLineBracesBlock(); - private IndentedStringBuilder Property(String name, T value, Boolean isLast = false) - where T : struct, Enum - => Property(name, Enum, value, isLast); - private IndentedStringBuilder Property(String name, Boolean value, Boolean isLast = false) - => Property(name, Append, value.ToString(), isLast); - private IndentedStringBuilder Property(String name, String value, Boolean isLast = false) - => Property(name, Literal, value, isLast); - private IndentedStringBuilder Property(String name, IEnumerable value, Boolean isLast = false) - => Property(name, Literals, value, isLast); - private IndentedStringBuilder Property( - String name, - Func valueAppend, - IEnumerable values, - Boolean isLast = false) - { - Append(name).AppendCore(": ["); - - using var enumerator = values.GetEnumerator(); - if(enumerator.MoveNext()) - _ = valueAppend.Invoke(enumerator.Current); - - while(enumerator.MoveNext()) - { - Append(',').AppendLineCore(); - _ = valueAppend.Invoke(enumerator.Current); - } - - AppendCore(']'); - if(!isLast) - Append(',').AppendLineCore(); - - return this; - } - private IndentedStringBuilder Property( - String name, - Func valueAppend, - T value, - Boolean isLast = false) - { - Append(name).AppendCore(": "); - _ = valueAppend.Invoke(value); - if(!isLast) - Append(',').AppendLineCore(); - - return this; - } - private IndentedStringBuilder Enum(T value) - where T : struct, Enum - => Append(value.ToString()); - private IndentedStringBuilder Literal(String literal) - { - LiteralCore(literal); - return this; - } - private IndentedStringBuilder Literals(IEnumerable literals) - { - AppendCore('['); - - using var enumerator = literals.GetEnumerator(); - if(enumerator.MoveNext()) - LiteralCore(enumerator.Current); - - while(enumerator.MoveNext()) - { - AppendCore(','); - LiteralCore(enumerator.Current); - } - - AppendCore(']'); - - return this; - } - private void LiteralCore(String literal) - { - if(literal == null) - AppendCore("null"); - else - Append('"').Append(literal).AppendCore('"'); - } - } -} diff --git a/UnionsGenerator/UnionsGenerator.csproj b/UnionsGenerator/UnionsGenerator.csproj deleted file mode 100644 index 4b97e8e..0000000 --- a/UnionsGenerator/UnionsGenerator.csproj +++ /dev/null @@ -1,58 +0,0 @@ - - - - - netstandard2.0 - false - true - true - true - - - - true - true - - Generate hybrid (tagged/type) union types. - - Source Generator; Union Types; Unions - https://raw.githubusercontent.com/PaulBraetz/RhoMicro.CodeAnalysis/master/UnionsGenerator/PackageLogo.svg - - - - $(DefineConstants);UNIONS_GENERATOR - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - diff --git a/UnionsGenerator/Usings.cs b/UnionsGenerator/Usings.cs deleted file mode 100644 index f84698c..0000000 --- a/UnionsGenerator/Usings.cs +++ /dev/null @@ -1,3 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -global using RhoMicro.CodeAnalysis.Library; diff --git a/UnionsGenerator/Utils/ConstantSources.cs b/UnionsGenerator/Utils/ConstantSources.cs deleted file mode 100644 index 5f28bb2..0000000 --- a/UnionsGenerator/Utils/ConstantSources.cs +++ /dev/null @@ -1,142 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Utils; - -using System; - -internal static partial class ConstantSources -{ - public static String GeneratedCode = $"[System.CodeDom.Compiler.GeneratedCodeAttribute(\"RhoMicro.CodeAnalysis.UnionsGenerator\", \"{typeof(ConstantSources).Assembly.GetName().Version}\")]"; - public const String EditorBrowsableNever = "[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]"; - public static String Util = - $$""" - // - // This file was generated by RhoMicro.CodeAnalysis.UnionsGenerator - // The tool used to generate this code may be subject to license terms; - // this generated code is however not subject to those terms, instead it is - // subject to the license (if any) applied to the containing project. - // - #nullable enable - #pragma warning disable - - namespace RhoMicro.CodeAnalysis.UnionsGenerator.Generated - { - using System.Collections.Concurrent; - using System.Collections.Generic; - using System.Text; - using System.Linq; - using System; - - {{GeneratedCode}} - {{EditorBrowsableNever}} - internal static class Util - { - private readonly static ConcurrentDictionary _cache = new(); - internal static String GetFullString(Type type) => _cache.GetOrAdd(type, ValueFactory); - static String ValueFactory(Type type) - { - var result = getString(type, new()); - - return result; - - static String getString(Type type, StringBuilder builder) - { - var unboundTransitiveParameters = 0; - var transitiveParameters = new List<(String Format, Type? Argument)>(); - append(type, builder, transitiveParameters, ref unboundTransitiveParameters); - var result = builder.ToString(); - - for(var i = 0; i < transitiveParameters.Count; i++) - { - _ = builder.Clear(); - var (format, argument) = transitiveParameters[i]; - var replacement = getString(argument!, builder); - result = result.Replace(format, replacement); - } - - return result; - - static void append( - Type type, - StringBuilder builder, - List<(String Format, Type? Argument)> transitiveArgumentsMap, - ref Int32 unboundTransitiveParameters) - { - if(type.IsGenericParameter && type.DeclaringMethod is null) - { - var format = $"{Guid.NewGuid()}"; - _ = builder.Append(format); - transitiveArgumentsMap.Add((format, null)); - unboundTransitiveParameters++; - return; - } else if(type.DeclaringType != null) - { - append(type.DeclaringType, builder, transitiveArgumentsMap, ref unboundTransitiveParameters); - _ = builder.Append('.'); - } else if(type.Namespace != null) - { - _ = builder.Append(type.Namespace) - .Append('.'); - } - - var tickIndex = type.Name.IndexOf('`'); - _ = tickIndex != -1 ? - builder.Append(type.Name.Substring(0, tickIndex)) : - builder.Append(type.Name); - - var arguments = type.GetGenericArguments(); - var inflectionPoint = unboundTransitiveParameters; - if(arguments.Length > 0 && unboundTransitiveParameters > 0) - { - for(; unboundTransitiveParameters > 0;) - { - unboundTransitiveParameters--; - var (format, _) = transitiveArgumentsMap[unboundTransitiveParameters]; - transitiveArgumentsMap[unboundTransitiveParameters] = (format, arguments[unboundTransitiveParameters]); - } - } - - if(arguments.Length > inflectionPoint) - { - _ = builder.Append('<'); - append(arguments[inflectionPoint], builder, transitiveArgumentsMap, ref unboundTransitiveParameters); - - for(var i = inflectionPoint + 1; i < type.GenericTypeArguments.Length; i++) - { - _ = builder.Append(", "); - append(arguments[i], builder, transitiveArgumentsMap, ref unboundTransitiveParameters); - } - - _ = builder.Append('>'); - } - } - } - } - - internal static System.Boolean IsMarked(Type type) => - type.CustomAttributes.Any(a => a.AttributeType.FullName == "{{Qualifications.NonGenericFullMetadataName}}") || - type.GenericTypeArguments.Any(t => t.CustomAttributes.Any(a => - a.AttributeType.FullName.StartsWith("{{Qualifications.GenericFullMetadataName}}") - && a.AttributeType.GenericTypeArguments.Length < {{Qualifications.MaxRepresentableTypesCount}})); - - private static readonly System.Collections.Concurrent.ConcurrentDictionary<(Type, Type), Object> _conversionImplementations = new(); - internal static TTo UnsafeConvert(in TFrom from) - { - var impl = (System.Func)_conversionImplementations.GetOrAdd((typeof(TFrom), typeof(TTo)), k => - { - var param = System.Linq.Expressions.Expression.Parameter(k.Item1); - var castExpr = System.Linq.Expressions.Expression.Convert(param, k.Item2); - var lambda = System.Linq.Expressions.Expression.Lambda(castExpr, param).Compile(); - - return lambda; - }); - var result = impl.Invoke(from); - - return result; - } - } - } - """; - - public const String InvalidTagStateThrow = "throw new System.InvalidOperationException(\"Unable to determine the represented type or value. The union type was likely not initialized correctly.\")"; -} diff --git a/UnionsGenerator/Utils/EnumerableExtensions.cs b/UnionsGenerator/Utils/EnumerableExtensions.cs deleted file mode 100644 index 7d6577c..0000000 --- a/UnionsGenerator/Utils/EnumerableExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Utils; -using System; -using System.Collections.Generic; -using System.Text; - -internal static class EnumerableExtensions -{ - public static IEnumerable DistinctBy(this IEnumerable values, Func discriminatorSelector) - { - var yieldedValueDiscriminators = new HashSet(); - foreach(var value in values) - { - var discriminator = discriminatorSelector.Invoke(value); - if(yieldedValueDiscriminators.Add(discriminator)) - yield return value; - } - } -} diff --git a/UnionsGenerator/Utils/Qualifications.cs b/UnionsGenerator/Utils/Qualifications.cs deleted file mode 100644 index ce22781..0000000 --- a/UnionsGenerator/Utils/Qualifications.cs +++ /dev/null @@ -1,139 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Utils; - -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -using System; -using System.Runtime.CompilerServices; -using System.Threading; - -internal static class Qualifications -{ - public const String NonGenericFullMetadataName = MetadataNamespace + "." + NonGenericMetadataName; - public const String NonGenericMetadataName = "UnionTypeAttribute"; - public const String MetadataNamespace = "RhoMicro.CodeAnalysis"; - public const String NonGenericRelationMetadataName = "RelationAttribute"; - public const String FactoryMetadataName = "UnionTypeFactoryAttribute"; - public const String GenericFullMetadataName = MetadataNamespace + "." + GenericMetadataName; - public const String GenericMetadataName = NonGenericMetadataName + "`"; - private static readonly Dictionary _genericNames = - Enumerable.Range(1, MaxRepresentableTypesCount).ToDictionary(i => i, i => $"{GenericFullMetadataName}{i}"); - public static String GetGenericFullMetadataName(Int32 typeParamCount) => _genericNames[typeParamCount]; - public static IEnumerable GenericMetadataNames => _genericNames.Values; - - public const Int32 MaxRepresentableTypesCount = 255; //limit to 255 due to tag type being byte + None tag - - public static Boolean IsUnionTypeSymbol(INamedTypeSymbol symbol, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - var attributes = symbol.GetAttributes(); - foreach(var attribute in attributes) - { - cancellationToken.ThrowIfCancellationRequested(); - if(IsUnionTypeAttribute(attribute) - || IsRelationAttribute(attribute) - || IsUnionTypeSettingsAttribute(attribute)) - { - return true; - } - } - - cancellationToken.ThrowIfCancellationRequested(); - var result = symbol is INamedTypeSymbol named && - named.GetMembers() - .OfType() - .Any(IsUnionTypeFactorySymbol); - - return result; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Boolean IsRelationAttribute(this AttributeData data) => - data.IsAttributesNamespaceAttribute() - && ( data.AttributeClass?.MetadataName.StartsWith(NonGenericRelationMetadataName) ?? false ); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Boolean IsAttributesNamespaceAttribute(this AttributeData data) => - data.AttributeClass?.ContainingNamespace.ToDisplayString( - SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)) - == MetadataNamespace; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Boolean IsUnionTypeDeclarationAttribute(this AttributeData data) => - data.IsAttributesNamespaceAttribute() - && data.AttributeClass!.MetadataName.StartsWith(GenericMetadataName) - && data.AttributeClass.TypeArguments.Length < MaxRepresentableTypesCount; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Boolean IsUnionTypeParameterAttribute(this AttributeData data) => - data.IsAttributesNamespaceAttribute() - && data.AttributeClass?.MetadataName == NonGenericMetadataName - && data.AttributeClass.TypeArguments.Length == 0; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Boolean IsUnionTypeAttribute(this AttributeData data) => data.IsUnionTypeParameterAttribute() || data.IsUnionTypeDeclarationAttribute(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Boolean IsUnionTypeSettingsAttribute(this AttributeData data) => - data.IsAttributesNamespaceAttribute() - && data.AttributeClass?.MetadataName == typeof(UnionTypeSettingsAttribute).Name; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Boolean IsUnionTypeParameterSyntax(SyntaxNode? node, CancellationToken cancellationToken) - { - //checking [UnionType] on type parameter - - cancellationToken.ThrowIfCancellationRequested(); - //only type parameters are valid targets - if(node is not TypeParameterSyntax tps) - return false; - - cancellationToken.ThrowIfCancellationRequested(); - //the containing declaration must be a type declaration that is itself a valid target (partial, class/struct etc) - //TypeParameter<-TypeParameterList<-TypeDeclaration - return IsUnionTypeDeclarationSyntax(tps.Parent?.Parent, cancellationToken); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Boolean IsUnionTypeDeclarationSyntax(SyntaxNode? node, CancellationToken cancellationToken) - { - //checking [UnionType] on type declaration - - cancellationToken.ThrowIfCancellationRequested(); - //only classes & structs are valid targets; records & interfaces are excluded - if(node is not ClassDeclarationSyntax and not StructDeclarationSyntax) - return false; - - cancellationToken.ThrowIfCancellationRequested(); - //declaration must be partial - return ( (TypeDeclarationSyntax)node ).Modifiers.Any(SyntaxKind.PartialKeyword); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Boolean IsUnionTypeFactoryAttribute(AttributeData data) => - data.IsAttributesNamespaceAttribute() && - data.AttributeClass?.MetadataName == FactoryMetadataName; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Boolean IsUnionTypeFactorySymbol(IMethodSymbol methodSymbol) - { - var result = methodSymbol.IsStatic - && !SymbolEqualityComparer.IncludeNullability.Equals(methodSymbol.ReturnType, methodSymbol.ContainingSymbol) - && methodSymbol.Parameters.Length == 1 - && methodSymbol.TypeParameters.Length == 0 - && methodSymbol.Parameters[0].GetAttributes().Any(IsUnionTypeFactoryAttribute); - - return result; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Boolean IsUnionTypeFactoryDeclarationSyntax(SyntaxNode? node, CancellationToken cancellationToken) - { - //checking [UnionTypeFactory] on static factory methods - - cancellationToken.ThrowIfCancellationRequested(); - //check that target is actually a method - if(node is not ParameterSyntax paramSyntax) - return false; - - cancellationToken.ThrowIfCancellationRequested(); - //check target method is static, takes one parameter and is not generic - return paramSyntax.Parent?.Parent is MethodDeclarationSyntax mds && - mds.Modifiers.Any(SyntaxKind.StaticKeyword) && - mds.ParameterList.Parameters.Count == 1 && - mds.TypeParameterList?.Parameters.Count is 0 or null; - } -} diff --git a/UnionsGenerator/Utils/Throw.cs b/UnionsGenerator/Utils/Throw.cs deleted file mode 100644 index bc1f477..0000000 --- a/UnionsGenerator/Utils/Throw.cs +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -namespace RhoMicro.CodeAnalysis.UnionsGenerator.Utils; -using System; -using System.Collections.Generic; -using System.Text; - -internal static class Throw -{ - public static void ArgumentNull(T value, String valueName) - where T : class => - _ = value ?? throw new ArgumentNullException(valueName); -} diff --git a/UnionsGenerator/tailwind.extension.json b/UnionsGenerator/tailwind.extension.json deleted file mode 100644 index f546671..0000000 --- a/UnionsGenerator/tailwind.extension.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/theron-wang/VS2022-Editor-Support-for-Tailwind-CSS/refs/heads/main/tailwind.extension.schema.json", - "ConfigurationFiles": [], - "BuildFiles": [], - "PackageConfigurationFile": null, - "CustomRegexes": { - "Razor": { - "Override": false, - "Values": [] - }, - "HTML": { - "Override": false, - "Values": [] - }, - "JavaScript": { - "Override": false, - "Values": [] - } - }, - "UseCli": false -} \ No newline at end of file diff --git a/dist/dev/RhoMicro.CodeAnalysis.UtilityGenerators.Dev.1.0.0.nupkg b/dist/dev/RhoMicro.CodeAnalysis.UtilityGenerators.Dev.1.0.0.nupkg index d2a05ac..fa86fdb 100644 Binary files a/dist/dev/RhoMicro.CodeAnalysis.UtilityGenerators.Dev.1.0.0.nupkg and b/dist/dev/RhoMicro.CodeAnalysis.UtilityGenerators.Dev.1.0.0.nupkg differ diff --git a/global.json b/global.json deleted file mode 100644 index 7da2763..0000000 --- a/global.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "sdk": { - "version": "8.0.100", - "rollForward": "latestMajor" - } -} \ No newline at end of file