diff --git a/Source/FunicularSwitch.Generators.Common/RoslynExtensions.cs b/Source/FunicularSwitch.Generators.Common/RoslynExtensions.cs index 2e394ca8..225def53 100644 --- a/Source/FunicularSwitch.Generators.Common/RoslynExtensions.cs +++ b/Source/FunicularSwitch.Generators.Common/RoslynExtensions.cs @@ -85,6 +85,14 @@ public static bool Implements(this INamedTypeSymbol symbol, ITypeSymbol interfac public static string FullTypeNameWithNamespaceAndGenerics(this INamedTypeSymbol namedTypeSymbol) => namedTypeSymbol.ToDisplayString(FullTypeWithNamespaceAndGenericsDisplayFormat); public static string FullTypeName(this INamedTypeSymbol namedTypeSymbol) => namedTypeSymbol.ToDisplayString(FullTypeDisplayFormat); + public static string PolytypeTypeofExpressionTypeName(this INamedTypeSymbol namedTypeSymbol) => namedTypeSymbol + .ToDisplayParts(format: FullTypeDisplayFormat + .WithGenericsOptions(SymbolDisplayGenericsOptions.IncludeTypeParameters)) + .Where(w => w.Kind is not (SymbolDisplayPartKind.TypeParameterName or SymbolDisplayPartKind.NamespaceName + or SymbolDisplayPartKind.Space)) + .ToImmutableArray() + .ToDisplayString(); + public static string FullNamespace(this INamespaceSymbol namespaceSymbol) => namespaceSymbol.ToDisplayString(FullTypeDisplayFormat); diff --git a/Source/FunicularSwitch.Generators/Analyzers/PolyTypeAnalyzer.cs b/Source/FunicularSwitch.Generators/Analyzers/PolyTypeAnalyzer.cs new file mode 100644 index 00000000..3fb17552 --- /dev/null +++ b/Source/FunicularSwitch.Generators/Analyzers/PolyTypeAnalyzer.cs @@ -0,0 +1,119 @@ +using System.Collections.Immutable; +using FunicularSwitch.Generators.Common; +using FunicularSwitch.Generators.UnionType; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Parser = FunicularSwitch.Generators.UnionType.Parser; + +namespace FunicularSwitch.Generators.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class PolyTypeAnalyzer : DiagnosticAnalyzer +{ + public const string DiagnosticId = "FS0010"; + + public static readonly DiagnosticDescriptor Rule = new( + id: DiagnosticId, + title: "Funicular Switch PolyType integration usage opportunity", + messageFormat: "Use DerivedTypeShape Attribute for PolyType support", + category: "Usage Opportunity", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public override ImmutableArray SupportedDiagnostics { get; } = [Rule]; + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSyntaxNodeAction(AnalyzeInvocation, SyntaxKind.Attribute); + } + + private void AnalyzeInvocation(SyntaxNodeAnalysisContext context) + { + if (context.Node is not AttributeSyntax attributeSyntax) + { + return; + } + + var (classSyntax, classSymbol, attributeData) = BaseTypeDeclarationSyntax(context.SemanticModel, attributeSyntax); + + if (classSyntax is null || attributeData is null || classSymbol is null) + { + return; + } + + if (attributeData.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) != + $"global::{UnionTypeGenerator.UnionTypeAttribute}") + { + return; + } + + var schema = Parser.GetUnionTypeSchema( + context.Compilation, + context.CancellationToken, + classSyntax, + classSymbol, + attributeData); + + var (unionTypeSchema, errors, hasValue) = schema; + + if (!hasValue || unionTypeSchema!.Cases.IsEmpty) + { + return; + } + + if (!context.Compilation.ReferencedAssemblyNames.Any(a => a.Name is "PolyType")) + { + return; + } + + var derivedAttributes = GetAttributesOfDerivedTypes(classSymbol); + + if (GetUnionTypeCasesWithoutAttribute(unionTypeSchema, derivedAttributes).Any()) + { + var diagnostic = Diagnostic.Create( + Rule, + attributeSyntax.GetLocation()); + + context.ReportDiagnostic(diagnostic); + } + } + + internal static (BaseTypeDeclarationSyntax? classSyntax, INamedTypeSymbol? classSymbol, AttributeData? attributeData) + BaseTypeDeclarationSyntax(SemanticModel semanticModel, AttributeSyntax attributeSyntax) + { + var attributeList = (attributeSyntax.Parent as AttributeListSyntax); + var classSyntax = (attributeList?.Parent as BaseTypeDeclarationSyntax); + if (classSyntax is null) + { + return (null, null, null); + } + + var classSymbol = semanticModel.GetDeclaredSymbol(classSyntax); + var attributeData = classSymbol?.GetAttributes() + .FirstOrDefault(a => a.ApplicationSyntaxReference!.Span == attributeSyntax.Span); + return (classSyntax, classSymbol, attributeData); + } + + internal static IEnumerable GetUnionTypeCasesWithoutAttribute(UnionTypeSchema unionTypeSchema, List derivedAttributes) + { + // ReSharper disable once SimplifyLinqExpressionUseAll + return unionTypeSchema.Cases + .Where(derivedType => !derivedAttributes + .Any(a => a == derivedType.PolyTypeTypeofExpressionName)); + } + + internal static List GetAttributesOfDerivedTypes(INamedTypeSymbol classSymbol) => + classSymbol + .GetAttributes() + .Where(a => a.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == + "global::PolyType.DerivedTypeShapeAttribute") + .Select(a => (a.ConstructorArguments[0].Value as INamedTypeSymbol)?.PolytypeTypeofExpressionTypeName()) + .Where(s => s is not null) + .Select(s => s!) + .ToList(); +} \ No newline at end of file diff --git a/Source/FunicularSwitch.Generators/CodeFixProviders/MatchNullCodeFixProvider.cs b/Source/FunicularSwitch.Generators/CodeFixProviders/MatchNullCodeFixProvider.cs index d19b0b3a..210ac74a 100644 --- a/Source/FunicularSwitch.Generators/CodeFixProviders/MatchNullCodeFixProvider.cs +++ b/Source/FunicularSwitch.Generators/CodeFixProviders/MatchNullCodeFixProvider.cs @@ -16,6 +16,11 @@ public class MatchNullCodeFixProvider : CodeFixProvider { public override ImmutableArray FixableDiagnosticIds { get; } = [MatchNullAnalyzer.DiagnosticId]; + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + public override async Task RegisterCodeFixesAsync(CodeFixContext context) { var diagnostic = context.Diagnostics.Single(); @@ -39,11 +44,12 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) context.RegisterCodeFix( CodeAction.Create( title: $"Use .Map().GetValueOrDefault()", - createChangedDocument: c => MigrateMatch(context.Document, diagnostic.Id, invocationExpressionSyntax, m, context.CancellationToken)), + equivalenceKey: diagnostic.Id, + createChangedDocument: c => MigrateMatch(context.Document, invocationExpressionSyntax, m, context.CancellationToken)), diagnostic); } - private async Task MigrateMatch(Document document, string diagnosticId, InvocationExpressionSyntax invocationExpressionSyntax, MemberAccessExpressionSyntax memberAccessExpression, CancellationToken cancellationToken) + private async Task MigrateMatch(Document document, InvocationExpressionSyntax invocationExpressionSyntax, MemberAccessExpressionSyntax memberAccessExpression, CancellationToken cancellationToken) { var updatedInvocationExpression = invocationExpressionSyntax .WithExpression(memberAccessExpression.WithName(IdentifierName("Map"))) diff --git a/Source/FunicularSwitch.Generators/CodeFixProviders/PolyTypeCodeFixProvider.cs b/Source/FunicularSwitch.Generators/CodeFixProviders/PolyTypeCodeFixProvider.cs new file mode 100644 index 00000000..e28b1e0d --- /dev/null +++ b/Source/FunicularSwitch.Generators/CodeFixProviders/PolyTypeCodeFixProvider.cs @@ -0,0 +1,116 @@ +using System.Collections.Immutable; +using System.Composition; +using FunicularSwitch.Generators.Analyzers; +using FunicularSwitch.Generators.UnionType; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Editing; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace FunicularSwitch.Generators.CodeFixProviders; + +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MatchNullCodeFixProvider)), Shared] +public class PolyTypeCodeFixProvider : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds { get; } = [PolyTypeAnalyzer.DiagnosticId]; + + public override FixAllProvider? GetFixAllProvider() + { + return WellKnownFixAllProviders.BatchFixer; + } + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var diagnostic = context.Diagnostics.Single(); + + var diagnosticSpan = diagnostic.Location.SourceSpan; + + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken); + + var diagnosticNode = root?.FindNode(diagnosticSpan); + + if (diagnosticNode is not AttributeSyntax attributeSyntax) + { + return; + } + + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken); + + if (semanticModel is null) + { + return; + } + + var (classSyntax, classSymbol, attributeData) = PolyTypeAnalyzer.BaseTypeDeclarationSyntax(semanticModel, attributeSyntax); + + if (classSyntax is null || classSymbol is null || attributeData is null) + { + return; + } + + var schema = Parser.GetUnionTypeSchema( + semanticModel.Compilation, + context.CancellationToken, + classSyntax, + classSymbol, + attributeData); + + var (unionTypeSchema, _, hasValue) = schema; + + if (!hasValue || unionTypeSchema is null) + { + return; + } + + bool hasPolyTypeUsing = + (root as CompilationUnitSyntax)?.Usings.Any(u => u.Name?.ToFullString() == "PolyType") ?? false; + + context.RegisterCodeFix( + CodeAction.Create( + title: $"Add DerivedTypeShape Attributes for union cases", + equivalenceKey: diagnostic.Id, + createChangedDocument: c => AddMissingAttributes( + context.Document, + classSyntax, + classSymbol, + unionTypeSchema, + context.CancellationToken, + hasPolyTypeUsing)), + diagnostic); + } + + private async Task AddMissingAttributes( + Document document, + BaseTypeDeclarationSyntax classSyntax, + INamedTypeSymbol classSymbol, + UnionTypeSchema unionTypeSchema, + CancellationToken cancellationToken, + bool hasPolyTypeUsing) + { + var derivedAttributes = PolyTypeAnalyzer.GetAttributesOfDerivedTypes(classSymbol); + + // ReSharper disable once SimplifyLinqExpressionUseAll + var casesWithoutAttributes = + PolyTypeAnalyzer.GetUnionTypeCasesWithoutAttribute(unionTypeSchema, derivedAttributes); + + var @namespace = !hasPolyTypeUsing ? "PolyType." : string.Empty; + + var newClassSyntax = classSyntax.AddAttributeLists( + casesWithoutAttributes.Select(c => + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName($"{@namespace}DerivedTypeShape")) + .WithArgumentList( + AttributeArgumentList( + SingletonSeparatedList( + AttributeArgument( + TypeOfExpression( + IdentifierName(c.PolyTypeTypeofExpressionName) + )))))))).ToArray()); + + var editor = await DocumentEditor.CreateAsync(document, cancellationToken); + editor.ReplaceNode(classSyntax, newClassSyntax); + return editor.GetChangedDocument(); + } +} \ No newline at end of file diff --git a/Source/FunicularSwitch.Generators/FunicularSwitch.Generators.csproj b/Source/FunicularSwitch.Generators/FunicularSwitch.Generators.csproj index 67f0204e..b7774863 100644 --- a/Source/FunicularSwitch.Generators/FunicularSwitch.Generators.csproj +++ b/Source/FunicularSwitch.Generators/FunicularSwitch.Generators.csproj @@ -17,7 +17,7 @@ 4 - 2.4 + 2.3 diff --git a/Source/FunicularSwitch.Generators/UnionType/Generator.cs b/Source/FunicularSwitch.Generators/UnionType/Generator.cs index 2f07dc40..43b88e8b 100644 --- a/Source/FunicularSwitch.Generators/UnionType/Generator.cs +++ b/Source/FunicularSwitch.Generators/UnionType/Generator.cs @@ -11,11 +11,8 @@ public static class Generator const string VoidMatchMethodName = "Switch"; const string MatchMethodName = "Match"; - public static (string filename, string source) Emit( - UnionTypeSchema unionTypeSchema, - bool hasPolyTypeReference, - Action reportDiagnostic, - CancellationToken cancellationToken) + public static (string filename, string source) Emit(UnionTypeSchema unionTypeSchema, + Action reportDiagnostic, CancellationToken cancellationToken) { var builder = new CSharpBuilder(); builder.WriteLine("#pragma warning disable 1591"); @@ -30,7 +27,7 @@ public static (string filename, string source) Emit( if (unionTypeSchema is { IsPartial: true, StaticFactoryInfo: not null }) { builder.WriteLine(""); - WritePartialWithStaticFactories(unionTypeSchema, hasPolyTypeReference, builder); + WritePartialWithStaticFactories(unionTypeSchema, builder); } } @@ -89,7 +86,7 @@ void BlankLine() } } - static void WritePartialWithStaticFactories(UnionTypeSchema unionTypeSchema, bool hasPolyTypeReference, CSharpBuilder builder) + static void WritePartialWithStaticFactories(UnionTypeSchema unionTypeSchema, CSharpBuilder builder) { var info = unionTypeSchema.StaticFactoryInfo!; var typeParameters = RoslynExtensions.FormatTypeParameters(unionTypeSchema.TypeParameters); @@ -98,13 +95,6 @@ static void WritePartialWithStaticFactories(UnionTypeSchema unionTypeSchema, boo var actualModifiers = unionTypeSchema.Modifiers .Select(m => m == "public" ? (unionTypeSchema.IsInternal ? "internal" : "public") : m); - if (hasPolyTypeReference) - { - foreach (var derivedType in unionTypeSchema.Cases) - { - builder.WriteLine($"[global::PolyType.{UnionTypeGenerator.DerivedTypeShapeAttribute}(typeof(global::{derivedType.FullTypeName}))]"); - } - } builder.WriteLine($"{(actualModifiers.ToSeparatedString(" "))} {typeKind} {unionTypeSchema.TypeName}{typeParameters}"); using (builder.Indent()) { diff --git a/Source/FunicularSwitch.Generators/UnionType/Parser.cs b/Source/FunicularSwitch.Generators/UnionType/Parser.cs index 1018618a..9fc955c6 100644 --- a/Source/FunicularSwitch.Generators/UnionType/Parser.cs +++ b/Source/FunicularSwitch.Generators/UnionType/Parser.cs @@ -212,7 +212,8 @@ static GenerationResult> ToOrderedCases(CaseOrder ca constructors: constructors, requiredMembers: requiredMembers, parameterName: parameterName, - staticFactoryMethodName: staticMethodName); + staticFactoryMethodName: staticMethodName, + polyTypeTypeofExpressionName: $"{d.symbol.PolytypeTypeofExpressionTypeName()}"); }).ToImmutableArray(); return new(derived, errors, true); diff --git a/Source/FunicularSwitch.Generators/UnionType/UnionTypeSchema.cs b/Source/FunicularSwitch.Generators/UnionType/UnionTypeSchema.cs index dd0aff58..0195f202 100644 --- a/Source/FunicularSwitch.Generators/UnionType/UnionTypeSchema.cs +++ b/Source/FunicularSwitch.Generators/UnionType/UnionTypeSchema.cs @@ -36,11 +36,14 @@ public sealed record DerivedType public string ParameterName { get; } public string StaticFactoryMethodName { get; } - public DerivedType(string fullTypeName, string parameterName, string staticFactoryMethodName, EquatableArray? constructors = null, EquatableArray? requiredMembers = null) + public string PolyTypeTypeofExpressionName { get; } + + public DerivedType(string fullTypeName, string parameterName, string staticFactoryMethodName, string polyTypeTypeofExpressionName, EquatableArray? constructors = null, EquatableArray? requiredMembers = null) { FullTypeName = fullTypeName; ParameterName = parameterName; StaticFactoryMethodName = staticFactoryMethodName; + PolyTypeTypeofExpressionName = polyTypeTypeofExpressionName; RequiredMembers = requiredMembers ?? []; Constructors = constructors ?? []; } diff --git a/Source/FunicularSwitch.Generators/UnionTypeGenerator.cs b/Source/FunicularSwitch.Generators/UnionTypeGenerator.cs index 01384125..8580b142 100644 --- a/Source/FunicularSwitch.Generators/UnionTypeGenerator.cs +++ b/Source/FunicularSwitch.Generators/UnionTypeGenerator.cs @@ -10,7 +10,6 @@ public class UnionTypeGenerator : IIncrementalGenerator { internal const string UnionTypeAttribute = "FunicularSwitch.Generators.UnionTypeAttribute"; internal const string UnionCaseAttribute = "FunicularSwitch.Generators.UnionCaseAttribute"; - internal const string DerivedTypeShapeAttribute = "DerivedTypeShapeAttribute"; public void Initialize(IncrementalGeneratorInitializationContext context) { @@ -23,43 +22,29 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .ForAttributeWithMetadataName( UnionTypeAttribute, predicate: static (_, _) => true, - transform: static (context, cancellationToken) => + transform: static (context, cancellationToken) => Parser.GetUnionTypeSchema( - context.SemanticModel.Compilation, - cancellationToken, + context.SemanticModel.Compilation, + cancellationToken, (BaseTypeDeclarationSyntax)context.TargetNode, (INamedTypeSymbol)context.TargetSymbol, context.Attributes[0] - ) - ) - .Combine(context.CompilationProvider - .SelectMany(static (compilation, _) => compilation.GlobalNamespace.GetNamespaceMembers()) - .Where(static (namespaceMember) => namespaceMember.Name == "PolyType") - .Select(static (namespaceMember, _) => - { - foreach (var namedTypeSymbol in namespaceMember.GetTypeMembers()) - { - if (namedTypeSymbol.Name == DerivedTypeShapeAttribute) - return true; - } - return false; - }) - .Where(t => t) - .Collect()); - + ) + ); + context.RegisterSourceOutput( unionTypeClasses, - static (spc, source) => Execute(source.Left, source.Right.Length > 0, spc)); + static (spc, source) => Execute(source, spc)); } - static void Execute(GenerationResult target, bool hasPolyTypeReference, SourceProductionContext context) + static void Execute(GenerationResult target, SourceProductionContext context) { var (unionTypeSchema, errors, hasValue) = target; foreach (var error in errors) context.ReportDiagnostic(error); if (!hasValue || unionTypeSchema!.Cases.IsEmpty) return; - var (filename, source) = Generator.Emit(unionTypeSchema, hasPolyTypeReference, context.ReportDiagnostic, context.CancellationToken); + var (filename, source) = Generator.Emit(unionTypeSchema, context.ReportDiagnostic, context.CancellationToken); context.AddSource(filename, source); } } \ No newline at end of file diff --git a/Source/Tests/FunicularSwitch.Generators.Consumer/Generated/FunicularSwitch.Generators/FunicularSwitch.Generators.UnionTypeGenerator/FunicularSwitchGeneratorsConsumerBaseTypeMatchExtension.g.cs b/Source/Tests/FunicularSwitch.Generators.Consumer/Generated/FunicularSwitch.Generators/FunicularSwitch.Generators.UnionTypeGenerator/FunicularSwitchGeneratorsConsumerBaseTypeMatchExtension.g.cs new file mode 100644 index 00000000..02941701 --- /dev/null +++ b/Source/Tests/FunicularSwitch.Generators.Consumer/Generated/FunicularSwitch.Generators/FunicularSwitch.Generators.UnionTypeGenerator/FunicularSwitchGeneratorsConsumerBaseTypeMatchExtension.g.cs @@ -0,0 +1,72 @@ +#pragma warning disable 1591 +#nullable enable +namespace FunicularSwitch.Generators.Consumer +{ + public static partial class BaseTypeMatchExtension + { + public static T Match(this global::FunicularSwitch.Generators.Consumer.BaseType baseType, global::System.Func derivedType, global::System.Func derivedType2) => + baseType switch + { + FunicularSwitch.Generators.Consumer.BaseType.DerivedType_ derivedType1 => derivedType(derivedType1), + FunicularSwitch.Generators.Consumer.BaseType.DerivedType2_ derivedType22 => derivedType2(derivedType22), + _ => throw new global::System.ArgumentException($"Unknown type derived from FunicularSwitch.Generators.Consumer.BaseType: {baseType.GetType().Name}") + }; + + public static global::System.Threading.Tasks.Task Match(this global::FunicularSwitch.Generators.Consumer.BaseType baseType, global::System.Func> derivedType, global::System.Func> derivedType2) => + baseType switch + { + FunicularSwitch.Generators.Consumer.BaseType.DerivedType_ derivedType1 => derivedType(derivedType1), + FunicularSwitch.Generators.Consumer.BaseType.DerivedType2_ derivedType22 => derivedType2(derivedType22), + _ => throw new global::System.ArgumentException($"Unknown type derived from FunicularSwitch.Generators.Consumer.BaseType: {baseType.GetType().Name}") + }; + + public static async global::System.Threading.Tasks.Task Match(this global::System.Threading.Tasks.Task baseType, global::System.Func derivedType, global::System.Func derivedType2) => + (await baseType.ConfigureAwait(false)).Match(derivedType, derivedType2); + + public static async global::System.Threading.Tasks.Task Match(this global::System.Threading.Tasks.Task baseType, global::System.Func> derivedType, global::System.Func> derivedType2) => + await (await baseType.ConfigureAwait(false)).Match(derivedType, derivedType2).ConfigureAwait(false); + + public static void Switch(this global::FunicularSwitch.Generators.Consumer.BaseType baseType, global::System.Action derivedType, global::System.Action derivedType2) + { + switch (baseType) + { + case FunicularSwitch.Generators.Consumer.BaseType.DerivedType_ derivedType1: + derivedType(derivedType1); + break; + case FunicularSwitch.Generators.Consumer.BaseType.DerivedType2_ derivedType22: + derivedType2(derivedType22); + break; + default: + throw new global::System.ArgumentException($"Unknown type derived from FunicularSwitch.Generators.Consumer.BaseType: {baseType.GetType().Name}"); + } + } + + public static async global::System.Threading.Tasks.Task Switch(this global::FunicularSwitch.Generators.Consumer.BaseType baseType, global::System.Func derivedType, global::System.Func derivedType2) + { + switch (baseType) + { + case FunicularSwitch.Generators.Consumer.BaseType.DerivedType_ derivedType1: + await derivedType(derivedType1).ConfigureAwait(false); + break; + case FunicularSwitch.Generators.Consumer.BaseType.DerivedType2_ derivedType22: + await derivedType2(derivedType22).ConfigureAwait(false); + break; + default: + throw new global::System.ArgumentException($"Unknown type derived from FunicularSwitch.Generators.Consumer.BaseType: {baseType.GetType().Name}"); + } + } + + public static async global::System.Threading.Tasks.Task Switch(this global::System.Threading.Tasks.Task baseType, global::System.Action derivedType, global::System.Action derivedType2) => + (await baseType.ConfigureAwait(false)).Switch(derivedType, derivedType2); + + public static async global::System.Threading.Tasks.Task Switch(this global::System.Threading.Tasks.Task baseType, global::System.Func derivedType, global::System.Func derivedType2) => + await (await baseType.ConfigureAwait(false)).Switch(derivedType, derivedType2).ConfigureAwait(false); + } + + public abstract partial record BaseType + { + public static FunicularSwitch.Generators.Consumer.BaseType DerivedType() => new FunicularSwitch.Generators.Consumer.BaseType.DerivedType_(); + public static FunicularSwitch.Generators.Consumer.BaseType DerivedType2() => new FunicularSwitch.Generators.Consumer.BaseType.DerivedType2_(); + } +} +#pragma warning restore 1591 diff --git a/Source/Tests/FunicularSwitch.Generators.Test/MatchNullAnalyzerTest.cs b/Source/Tests/FunicularSwitch.Generators.Test/AnalyzerTests/MatchNullAnalyzerTest.cs similarity index 100% rename from Source/Tests/FunicularSwitch.Generators.Test/MatchNullAnalyzerTest.cs rename to Source/Tests/FunicularSwitch.Generators.Test/AnalyzerTests/MatchNullAnalyzerTest.cs diff --git a/Source/Tests/FunicularSwitch.Generators.Test/AnalyzerTests/PolyTypeAnalyzerTests.cs b/Source/Tests/FunicularSwitch.Generators.Test/AnalyzerTests/PolyTypeAnalyzerTests.cs new file mode 100644 index 00000000..a3916661 --- /dev/null +++ b/Source/Tests/FunicularSwitch.Generators.Test/AnalyzerTests/PolyTypeAnalyzerTests.cs @@ -0,0 +1,113 @@ +using FluentAssertions; +using FunicularSwitch.Generators.Analyzers; +using FunicularSwitch.Generators.CodeFixProviders; + +namespace FunicularSwitch.Generators.Test.AnalyzerTests; + +[TestClass] +public class PolyTypeAnalyzerTests : VerifyAnalyzer +{ + public PolyTypeAnalyzerTests() : base() + { + } + + [TestMethod] + public async Task PolyTypeDerivedAttribute_MissingIsRecognized_FixIsApplied() + { + var code = + """ + using FunicularSwitch.Generators; + + namespace FunicularSwitch.Test; + + [UnionType] + public abstract partial record BaseType + { + public sealed record DerivedType_() : BaseType; + public sealed record DerivedType2_() : BaseType; + } + """; + await Verify( + code, + new PolyTypeAnalyzer(), + new PolyTypeCodeFixProvider(), + diagnostic => diagnostic.Should().ContainSingle().Which.Id.Should().Be("FS0010"), + hasPolytypeReference: true); + } + + [TestMethod] + public async Task PolyTypeDerivedAttribute_PartialMissingIsRecognized_FixIsApplied() + { + var code = + """ + using FunicularSwitch.Generators; + + namespace FunicularSwitch.Test; + + [UnionType] + [PolyType.DerivedTypeShape(typeof(BaseType.DerivedType_))] + public abstract partial record BaseType + { + public sealed record DerivedType_() : BaseType; + public sealed record DerivedType2_() : BaseType; + } + """; + await Verify( + code, + new PolyTypeAnalyzer(), + new PolyTypeCodeFixProvider(), + diagnostic => diagnostic.Should().ContainSingle().Which.Id.Should().Be("FS0010"), + hasPolytypeReference: true); + } + + [TestMethod] + public async Task PolyTypeDerivedAttribute_PartialMissingWithGenerics_FixIsApplied() + { + var code = + """ + using FunicularSwitch.Generators; + + namespace FunicularSwitch.Test.Generic; + + [UnionType(CaseOrder = CaseOrder.AsDeclared)] + [PolyType.DerivedTypeShape(typeof(GenericResult<,>.Ok_))] + public abstract partial record GenericResult(bool IsOk) + { + public record Ok_(T Value) : GenericResult(true); + public record Error_(TFailure Failure) : GenericResult(false); + } + """; + await Verify( + code, + new PolyTypeAnalyzer(), + new PolyTypeCodeFixProvider(), + diagnostic => diagnostic.Should().ContainSingle().Which.Id.Should().Be("FS0010"), + hasPolytypeReference: true); + } + + [TestMethod] + public async Task PolyTypeDerivedAttribute_HasPolyTypeReferenced_NamespaceIsOmitted() + { + var code = + """ + using FunicularSwitch.Generators; + using PolyType; + + namespace FunicularSwitch.Test; + + [UnionType] + public abstract partial record BaseType + { + public sealed record DerivedType_() : BaseType; + public sealed record DerivedType2_() : BaseType; + } + """;; + await Verify( + code, + new PolyTypeAnalyzer(), + new PolyTypeCodeFixProvider(), + diagnostic => diagnostic.Should().ContainSingle().Which.Id.Should().Be("FS0010"), + hasPolytypeReference: true); + } + +} \ No newline at end of file diff --git a/Source/Tests/FunicularSwitch.Generators.Test/FunicularSwitch.Generators.Test.csproj b/Source/Tests/FunicularSwitch.Generators.Test/FunicularSwitch.Generators.Test.csproj index b6a2ad02..7cb27ac1 100644 --- a/Source/Tests/FunicularSwitch.Generators.Test/FunicularSwitch.Generators.Test.csproj +++ b/Source/Tests/FunicularSwitch.Generators.Test/FunicularSwitch.Generators.Test.csproj @@ -21,4 +21,8 @@ + + + + diff --git a/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/PolyTypeAnalyzerTests_PolyTypeDerivedAttribute_HasPolyTypeReferenced_NamespaceIsOmitted_FS0010_0.cs.verified.txt b/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/PolyTypeAnalyzerTests_PolyTypeDerivedAttribute_HasPolyTypeReferenced_NamespaceIsOmitted_FS0010_0.cs.verified.txt new file mode 100644 index 00000000..3ca62f84 --- /dev/null +++ b/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/PolyTypeAnalyzerTests_PolyTypeDerivedAttribute_HasPolyTypeReferenced_NamespaceIsOmitted_FS0010_0.cs.verified.txt @@ -0,0 +1,13 @@ +using FunicularSwitch.Generators; +using PolyType; + +namespace FunicularSwitch.Test; + +[UnionType] +[DerivedTypeShape(typeof(BaseType.DerivedType_))] +[DerivedTypeShape(typeof(BaseType.DerivedType2_))] +public abstract partial record BaseType +{ + public sealed record DerivedType_() : BaseType; + public sealed record DerivedType2_() : BaseType; +} \ No newline at end of file diff --git a/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/PolyTypeAnalyzerTests_PolyTypeDerivedAttribute_MissingIsRecognized_FixIsApplied_FS0010_0.cs.verified.txt b/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/PolyTypeAnalyzerTests_PolyTypeDerivedAttribute_MissingIsRecognized_FixIsApplied_FS0010_0.cs.verified.txt new file mode 100644 index 00000000..4cc0bb87 --- /dev/null +++ b/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/PolyTypeAnalyzerTests_PolyTypeDerivedAttribute_MissingIsRecognized_FixIsApplied_FS0010_0.cs.verified.txt @@ -0,0 +1,12 @@ +using FunicularSwitch.Generators; + +namespace FunicularSwitch.Test; + +[UnionType] +[PolyType.DerivedTypeShape(typeof(BaseType.DerivedType_))] +[PolyType.DerivedTypeShape(typeof(BaseType.DerivedType2_))] +public abstract partial record BaseType +{ + public sealed record DerivedType_() : BaseType; + public sealed record DerivedType2_() : BaseType; +} \ No newline at end of file diff --git a/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/PolyTypeAnalyzerTests_PolyTypeDerivedAttribute_PartialMissingIsRecognized_FixIsApplied_FS0010_0.cs.verified.txt b/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/PolyTypeAnalyzerTests_PolyTypeDerivedAttribute_PartialMissingIsRecognized_FixIsApplied_FS0010_0.cs.verified.txt new file mode 100644 index 00000000..4cc0bb87 --- /dev/null +++ b/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/PolyTypeAnalyzerTests_PolyTypeDerivedAttribute_PartialMissingIsRecognized_FixIsApplied_FS0010_0.cs.verified.txt @@ -0,0 +1,12 @@ +using FunicularSwitch.Generators; + +namespace FunicularSwitch.Test; + +[UnionType] +[PolyType.DerivedTypeShape(typeof(BaseType.DerivedType_))] +[PolyType.DerivedTypeShape(typeof(BaseType.DerivedType2_))] +public abstract partial record BaseType +{ + public sealed record DerivedType_() : BaseType; + public sealed record DerivedType2_() : BaseType; +} \ No newline at end of file diff --git a/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/PolyTypeAnalyzerTests_PolyTypeDerivedAttribute_PartialMissingWithGenerics_FixIsApplied_FS0010_0.cs.verified.txt b/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/PolyTypeAnalyzerTests_PolyTypeDerivedAttribute_PartialMissingWithGenerics_FixIsApplied_FS0010_0.cs.verified.txt new file mode 100644 index 00000000..8b92d141 --- /dev/null +++ b/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/PolyTypeAnalyzerTests_PolyTypeDerivedAttribute_PartialMissingWithGenerics_FixIsApplied_FS0010_0.cs.verified.txt @@ -0,0 +1,12 @@ +using FunicularSwitch.Generators; + +namespace FunicularSwitch.Test.Generic; + +[UnionType(CaseOrder = CaseOrder.AsDeclared)] +[PolyType.DerivedTypeShape(typeof(GenericResult<,>.Ok_))] +[PolyType.DerivedTypeShape(typeof(GenericResult<,>.Error_))] +public abstract partial record GenericResult(bool IsOk) +{ + public record Ok_(T Value) : GenericResult(true); + public record Error_(TFailure Failure) : GenericResult(false); +} \ No newline at end of file diff --git a/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/Run_union_type_generator.ForUnionType_WhenReferencingPolyType_GeneratesAttributes#Attributes.g.00.verified.cs b/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/Run_union_type_generator.ForUnionType_WhenReferencingPolyType_GeneratesAttributes#Attributes.g.00.verified.cs deleted file mode 100644 index 3336ef53..00000000 --- a/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/Run_union_type_generator.ForUnionType_WhenReferencingPolyType_GeneratesAttributes#Attributes.g.00.verified.cs +++ /dev/null @@ -1,41 +0,0 @@ -//HintName: Attributes.g.cs -#nullable enable -using System; - -// ReSharper disable once CheckNamespace -namespace FunicularSwitch.Generators -{ - /// - /// Mark an abstract partial type with a single generic argument with the ResultType attribute. - /// This type from now on has Ok | Error semantics with map and bind operations. - /// - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - sealed class ResultTypeAttribute : Attribute - { - public ResultTypeAttribute() => ErrorType = typeof(string); - public ResultTypeAttribute(Type errorType) => ErrorType = errorType; - - public Type ErrorType { get; set; } - } - - /// - /// Mark a static method or a member method or you error type with the MergeErrorAttribute attribute. - /// Static signature: TError -> TError -> TError. Member signature: TError -> TError - /// We are now able to collect errors and methods like Validate, Aggregate, FirstOk that are useful to combine results are generated. - /// - [AttributeUsage(AttributeTargets.Method, Inherited = false)] - sealed class MergeErrorAttribute : Attribute - { - } - - /// - /// Mark a static method with the ExceptionToError attribute. - /// Signature: Exception -> TError - /// This method is always called, when an exception happens in a bind operation. - /// So a call like result.Map(i => i/0) will return an Error produced by the factory method instead of throwing the DivisionByZero exception. - /// - [AttributeUsage(AttributeTargets.Method, Inherited = false)] - sealed class ExceptionToError : Attribute - { - } -} \ No newline at end of file diff --git a/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/Run_union_type_generator.ForUnionType_WhenReferencingPolyType_GeneratesAttributes#Attributes.g.01.verified.cs b/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/Run_union_type_generator.ForUnionType_WhenReferencingPolyType_GeneratesAttributes#Attributes.g.01.verified.cs deleted file mode 100644 index 3d6aae40..00000000 --- a/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/Run_union_type_generator.ForUnionType_WhenReferencingPolyType_GeneratesAttributes#Attributes.g.01.verified.cs +++ /dev/null @@ -1,29 +0,0 @@ -//HintName: Attributes.g.cs -#nullable enable -using System; - -// ReSharper disable once CheckNamespace -namespace FunicularSwitch.Generators -{ - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, Inherited = false)] - sealed class UnionTypeAttribute : Attribute - { - public CaseOrder CaseOrder { get; set; } = CaseOrder.Alphabetic; - public bool StaticFactoryMethods { get; set; } = true; - } - - enum CaseOrder - { - Alphabetic, - AsDeclared, - Explicit - } - - [AttributeUsage(AttributeTargets.Class, Inherited = false)] - sealed class UnionCaseAttribute : Attribute - { - public UnionCaseAttribute(int index) => Index = index; - - public int Index { get; } - } -} \ No newline at end of file diff --git a/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/Run_union_type_generator.ForUnionType_WhenReferencingPolyType_GeneratesAttributes#Attributes.g.02.verified.cs b/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/Run_union_type_generator.ForUnionType_WhenReferencingPolyType_GeneratesAttributes#Attributes.g.02.verified.cs deleted file mode 100644 index ad0ffe0d..00000000 --- a/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/Run_union_type_generator.ForUnionType_WhenReferencingPolyType_GeneratesAttributes#Attributes.g.02.verified.cs +++ /dev/null @@ -1,62 +0,0 @@ -//HintName: Attributes.g.cs -#nullable enable -using System; - -// ReSharper disable once CheckNamespace -namespace FunicularSwitch.Generators -{ - [AttributeUsage(AttributeTargets.Enum)] - sealed class ExtendedEnumAttribute : Attribute - { - public EnumCaseOrder CaseOrder { get; set; } = EnumCaseOrder.AsDeclared; - public ExtensionAccessibility Accessibility { get; set; } = ExtensionAccessibility.Public; - } - - enum EnumCaseOrder - { - Alphabetic, - AsDeclared - } - - /// - /// Generate match methods for all enums defined in assembly that contains AssemblySpecifier. - /// - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - class ExtendEnumsAttribute : Attribute - { - public Type AssemblySpecifier { get; } - public EnumCaseOrder CaseOrder { get; set; } = EnumCaseOrder.AsDeclared; - public ExtensionAccessibility Accessibility { get; set; } = ExtensionAccessibility.Public; - - public ExtendEnumsAttribute() => AssemblySpecifier = typeof(ExtendEnumsAttribute); - - public ExtendEnumsAttribute(Type assemblySpecifier) - { - AssemblySpecifier = assemblySpecifier; - } - } - - /// - /// Generate match methods for Type. Must be enum. - /// - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - class ExtendEnumAttribute : Attribute - { - public Type Type { get; } - - public EnumCaseOrder CaseOrder { get; set; } = EnumCaseOrder.AsDeclared; - - public ExtensionAccessibility Accessibility { get; set; } = ExtensionAccessibility.Public; - - public ExtendEnumAttribute(Type type) - { - Type = type; - } - } - - enum ExtensionAccessibility - { - Internal, - Public - } -} \ No newline at end of file diff --git a/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/Run_union_type_generator.ForUnionType_WhenReferencingPolyType_GeneratesAttributes#FunicularSwitchTextBaseTypeMatchExtension.g.verified.cs b/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/Run_union_type_generator.ForUnionType_WhenReferencingPolyType_GeneratesAttributes#FunicularSwitchTextBaseTypeMatchExtension.g.verified.cs deleted file mode 100644 index 0d16e806..00000000 --- a/Source/Tests/FunicularSwitch.Generators.Test/Snapshots/Run_union_type_generator.ForUnionType_WhenReferencingPolyType_GeneratesAttributes#FunicularSwitchTextBaseTypeMatchExtension.g.verified.cs +++ /dev/null @@ -1,75 +0,0 @@ -//HintName: FunicularSwitchTextBaseTypeMatchExtension.g.cs -#pragma warning disable 1591 -#nullable enable -namespace FunicularSwitch.Text -{ - public static partial class BaseTypeMatchExtension - { - public static T Match(this global::FunicularSwitch.Text.BaseType baseType, global::System.Func derivedType, global::System.Func derivedType2) => - baseType switch - { - FunicularSwitch.Text.BaseType.DerivedType_ derivedType1 => derivedType(derivedType1), - FunicularSwitch.Text.BaseType.DerivedType2_ derivedType22 => derivedType2(derivedType22), - _ => throw new global::System.ArgumentException($"Unknown type derived from FunicularSwitch.Text.BaseType: {baseType.GetType().Name}") - }; - - public static global::System.Threading.Tasks.Task Match(this global::FunicularSwitch.Text.BaseType baseType, global::System.Func> derivedType, global::System.Func> derivedType2) => - baseType switch - { - FunicularSwitch.Text.BaseType.DerivedType_ derivedType1 => derivedType(derivedType1), - FunicularSwitch.Text.BaseType.DerivedType2_ derivedType22 => derivedType2(derivedType22), - _ => throw new global::System.ArgumentException($"Unknown type derived from FunicularSwitch.Text.BaseType: {baseType.GetType().Name}") - }; - - public static async global::System.Threading.Tasks.Task Match(this global::System.Threading.Tasks.Task baseType, global::System.Func derivedType, global::System.Func derivedType2) => - (await baseType.ConfigureAwait(false)).Match(derivedType, derivedType2); - - public static async global::System.Threading.Tasks.Task Match(this global::System.Threading.Tasks.Task baseType, global::System.Func> derivedType, global::System.Func> derivedType2) => - await (await baseType.ConfigureAwait(false)).Match(derivedType, derivedType2).ConfigureAwait(false); - - public static void Switch(this global::FunicularSwitch.Text.BaseType baseType, global::System.Action derivedType, global::System.Action derivedType2) - { - switch (baseType) - { - case FunicularSwitch.Text.BaseType.DerivedType_ derivedType1: - derivedType(derivedType1); - break; - case FunicularSwitch.Text.BaseType.DerivedType2_ derivedType22: - derivedType2(derivedType22); - break; - default: - throw new global::System.ArgumentException($"Unknown type derived from FunicularSwitch.Text.BaseType: {baseType.GetType().Name}"); - } - } - - public static async global::System.Threading.Tasks.Task Switch(this global::FunicularSwitch.Text.BaseType baseType, global::System.Func derivedType, global::System.Func derivedType2) - { - switch (baseType) - { - case FunicularSwitch.Text.BaseType.DerivedType_ derivedType1: - await derivedType(derivedType1).ConfigureAwait(false); - break; - case FunicularSwitch.Text.BaseType.DerivedType2_ derivedType22: - await derivedType2(derivedType22).ConfigureAwait(false); - break; - default: - throw new global::System.ArgumentException($"Unknown type derived from FunicularSwitch.Text.BaseType: {baseType.GetType().Name}"); - } - } - - public static async global::System.Threading.Tasks.Task Switch(this global::System.Threading.Tasks.Task baseType, global::System.Action derivedType, global::System.Action derivedType2) => - (await baseType.ConfigureAwait(false)).Switch(derivedType, derivedType2); - - public static async global::System.Threading.Tasks.Task Switch(this global::System.Threading.Tasks.Task baseType, global::System.Func derivedType, global::System.Func derivedType2) => - await (await baseType.ConfigureAwait(false)).Switch(derivedType, derivedType2).ConfigureAwait(false); - } - - [global::PolyType.DerivedTypeShapeAttribute(typeof(global::FunicularSwitch.Text.BaseType.DerivedType_))] - [global::PolyType.DerivedTypeShapeAttribute(typeof(global::FunicularSwitch.Text.BaseType.DerivedType2_))] - public abstract partial record BaseType - { - public static FunicularSwitch.Text.BaseType DerivedType() => new FunicularSwitch.Text.BaseType.DerivedType_(); - public static FunicularSwitch.Text.BaseType DerivedType2() => new FunicularSwitch.Text.BaseType.DerivedType2_(); - } -} -#pragma warning restore 1591 diff --git a/Source/Tests/FunicularSwitch.Generators.Test/UnionTypeGeneratorSpecs.cs b/Source/Tests/FunicularSwitch.Generators.Test/UnionTypeGeneratorSpecs.cs index 54b957d6..a4c1a3c6 100644 --- a/Source/Tests/FunicularSwitch.Generators.Test/UnionTypeGeneratorSpecs.cs +++ b/Source/Tests/FunicularSwitch.Generators.Test/UnionTypeGeneratorSpecs.cs @@ -544,24 +544,4 @@ public sealed record DerivedType_(string? NullableReferenceType, int? NullableSt return Verify(code); } - - [TestMethod] - public Task ForUnionType_WhenReferencingPolyType_GeneratesAttributes() - { - var code = """ - using FunicularSwitch.Generators; - - namespace FunicularSwitch.Text; - - [UnionType] - public abstract partial record BaseType - { - public sealed record DerivedType_() : BaseType; - - public sealed record DerivedType2_() : BaseType; - } - """; - - return Verify(code, referencePolyType: true); - } } \ No newline at end of file diff --git a/Source/Tests/FunicularSwitch.Generators.Test/VerifyAnalyzer.cs b/Source/Tests/FunicularSwitch.Generators.Test/VerifyAnalyzer.cs index 62bc612a..d36248f9 100644 --- a/Source/Tests/FunicularSwitch.Generators.Test/VerifyAnalyzer.cs +++ b/Source/Tests/FunicularSwitch.Generators.Test/VerifyAnalyzer.cs @@ -1,12 +1,14 @@ using System.Collections.Immutable; using System.Runtime.CompilerServices; using FluentAssertions; +using FluentAssertions.Execution; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Text; +using PolyType; namespace FunicularSwitch.Generators.Test; @@ -22,6 +24,7 @@ public class VerifyAnalyzer : VerifyBase private static readonly MetadataReference CSharpSymbolsReference = MetadataReference.CreateFromFile(typeof(CSharpCompilation).Assembly.Location); private static readonly MetadataReference CodeAnalysisReference = MetadataReference.CreateFromFile(typeof(Compilation).Assembly.Location); private static readonly MetadataReference FunicularSwitchReference = MetadataReference.CreateFromFile(typeof(Unit).Assembly.Location); + private static readonly MetadataReference PolyTypeReference = MetadataReference.CreateFromFile(typeof(DerivedTypeShapeAttribute).Assembly.Location); // ReSharper disable once ExplicitCallerInfoArgument protected VerifyAnalyzer([CallerFilePath] string sourceFile = "") @@ -39,6 +42,7 @@ protected async Task Verify( CodeFixProvider codeFixProvider, Action> verifyDiagnostics, Action? verifyCodeAction = null, + bool hasPolytypeReference = false, [CallerMemberName] string callingMethod = "") { const string TestProjectName = "Test"; @@ -58,17 +62,23 @@ protected async Task Verify( .AddMetadataReference(projectId, CSharpSymbolsReference) .AddMetadataReference(projectId, CodeAnalysisReference) .AddMetadataReference(projectId, FunicularSwitchReference) - .WithProjectParseOptions(projectId, new CSharpParseOptions(LanguageVersion.Preview)) + .WithProjectParseOptions(projectId, new CSharpParseOptions(LanguageVersion.Latest)) .WithProjectCompilationOptions(projectId, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)) .AddDocument(documentId, fileName, SourceText.From(source)); + if (hasPolytypeReference) + { + solution = solution.AddMetadataReference(projectId, PolyTypeReference); + } + var project = solution.GetProject(projectId)!; - var compilationWithAnalyzers = (await CheckCompilation(project))!.WithAnalyzers([analyzer]); + var compilationWithAnalyzers = (await CheckCompilation(project)).Item1!.WithAnalyzers([analyzer]); var diagnostics = await compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync(); verifyDiagnostics(diagnostics); var document = project.Documents.First(); var index = 0; + using var scope = new AssertionScope(); foreach (var d in diagnostics .OrderBy(d => d.Location.GetLineSpan().StartLinePosition.Line) .ThenBy(d => d.Location.GetLineSpan().StartLinePosition.Character)) @@ -80,8 +90,10 @@ protected async Task Verify( verifyCodeAction?.Invoke(d, actions[0]); var updatedDocument = await ApplyFix(document, actions[0]); var syntaxTree = await updatedDocument.GetSyntaxRootAsync(); - var updatedCode = syntaxTree.ToFullString(); - await CheckCompilation(solution.RemoveDocument(documentId).AddDocument(documentId, fileName, SourceText.From(updatedCode)).GetProject(projectId)!); + var updatedCode = syntaxTree!.ToFullString(); + var (_, updatedDiagnostics) = await CheckCompilation(solution.RemoveDocument(documentId).AddDocument(documentId, fileName, SourceText.From(updatedCode)).GetProject(projectId)!); + + updatedDiagnostics.Should().BeEmpty(); var settings = new VerifySettings(); settings.UseFileName($"{Path.GetFileNameWithoutExtension(this.sourceFile)}_{callingMethod}_{d.Id}_{index}.cs"); @@ -90,13 +102,18 @@ await Verify(updatedCode, settings) index += 1; } - async Task CheckCompilation(Project p) + async Task<(Compilation Compilation, ImmutableArray Diagnostics)> CheckCompilation(Project p) { var c = await p.GetCompilationAsync()!; - c.GetDiagnostics() + + GeneratorDriver driver = CSharpGeneratorDriver.Create(new UnionTypeGenerator()); + + driver = driver.RunGeneratorsAndUpdateCompilation(c!, out var updatedCompilation, out var dA); + + updatedCompilation.GetDiagnostics() .Where(d => d.Severity == DiagnosticSeverity.Error) .Should().BeEmpty(); - return c; + return (c, dA)!; } } diff --git a/Source/Tests/FunicularSwitch.Generators.Test/VerifySourceGenerator.cs b/Source/Tests/FunicularSwitch.Generators.Test/VerifySourceGenerator.cs index 8a5133b0..507edad6 100644 --- a/Source/Tests/FunicularSwitch.Generators.Test/VerifySourceGenerator.cs +++ b/Source/Tests/FunicularSwitch.Generators.Test/VerifySourceGenerator.cs @@ -1,28 +1,27 @@ +using System; using System.Collections.Immutable; -using System.Reflection; +using System.IO; +using System.Linq; +using System.Threading.Tasks; using FluentAssertions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using PolyType; +using VerifyMSTest; namespace FunicularSwitch.Generators.Test; public abstract class VerifySourceGenerator : VerifyBase { - protected Task Verify(string source, IReadOnlyList additionalAssemblies, Action>? verifyCompilation) + protected Task Verify(string source, Action>? verifyCompilation) { var syntaxTree = CSharpSyntaxTree.ParseText(source); var assemblyDirectory = Path.GetDirectoryName(typeof(object).Assembly.Location)!; - var references = - new[] - { - typeof(object), - typeof(Enumerable), - } - .Select(t => t.Assembly) - .Concat(additionalAssemblies) - .Select(a => MetadataReference.CreateFromFile(a.Location)) + var references = new[] + { + typeof(object), + typeof(Enumerable) + }.Select(t => MetadataReference.CreateFromFile(t.Assembly.Location)) .Concat([ MetadataReference.CreateFromFile(Path.Combine(assemblyDirectory, "System.Runtime.dll")), MetadataReference.CreateFromFile(Path.Combine(assemblyDirectory, "System.Collections.dll")) @@ -46,8 +45,8 @@ protected Task Verify(string source, IReadOnlyList additionalAssemblie .UseDirectory("Snapshots"); } - protected Task Verify(string source, bool referencePolyType = false) => - Verify(source, additionalAssemblies: referencePolyType ? [typeof(DerivedTypeShapeAttribute).Assembly] : [], (compilation, _) => + protected Task Verify(string source) => + Verify(source, (compilation, _) => { var diagnostics = compilation.GetDiagnostics(); var errors = string.Join(Environment.NewLine, diagnostics