From 77a2d061cdbee5b79c8e34ba8db3353ecfbef7d4 Mon Sep 17 00:00:00 2001 From: Yarin Omesi Date: Fri, 4 Aug 2023 17:06:52 +0300 Subject: [PATCH 1/4] Create ResultClassEqualityComparer and use it --- .../IncrementalMockFillerSourceGenerator.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/TestsHelper.SourceGenerator/SourceGeneratorImplementations/IncrementalMockFillerSourceGenerator.cs b/TestsHelper.SourceGenerator/SourceGeneratorImplementations/IncrementalMockFillerSourceGenerator.cs index f97be41..76e90ef 100644 --- a/TestsHelper.SourceGenerator/SourceGeneratorImplementations/IncrementalMockFillerSourceGenerator.cs +++ b/TestsHelper.SourceGenerator/SourceGeneratorImplementations/IncrementalMockFillerSourceGenerator.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -21,7 +22,8 @@ public class IncrementalMockFillerSourceGenerator : IIncrementalGenerator public void Initialize(IncrementalGeneratorInitializationContext context) { IncrementalValuesProvider syntaxProvider = context.SyntaxProvider - .CreateSyntaxProvider(Predicate, Transform); + .CreateSyntaxProvider(Predicate, Transform) + .WithComparer(ResultClassEqualityComparer.Instance); context.RegisterSourceOutput(syntaxProvider.Collect(), Execute); } @@ -83,4 +85,15 @@ private static void Execute(SourceProductionContext context, ImmutableArray Diagnostics, TestClassMockCandidate? ClassToFillMockIn = null); + + private sealed class ResultClassEqualityComparer : IEqualityComparer + { + public static ResultClassEqualityComparer Instance { get; } = new ResultClassEqualityComparer(); + public bool Equals(ResultClass x, ResultClass y) + { + return x.Diagnostics.SequenceEqual(y.Diagnostics) && + Nullable.Equals(x.ClassToFillMockIn, y.ClassToFillMockIn); + } + public int GetHashCode(ResultClass obj) => throw new NotImplementedException(); + } } \ No newline at end of file From c48a648d136d8f4c86b27f041a345f56df4e92c0 Mon Sep 17 00:00:00 2001 From: Yarin Omesi Date: Fri, 4 Aug 2023 17:10:19 +0300 Subject: [PATCH 2/4] ignore not partial classes * Fix test * Remove `ClassIsNotPartial` diagnostic * Remove diagnostic report in `TestClassMockCandidateFactory` --- .../MockFillerSourceGeneratorTests.cs | 7 ++----- .../Diagnostics/DiagnosticRegistry.cs | 9 --------- .../MockFilling/TestClassMockCandidateFactory.cs | 13 +++---------- .../IncrementalMockFillerSourceGenerator.cs | 3 ++- 4 files changed, 7 insertions(+), 25 deletions(-) diff --git a/TestsHelper.SourceGenerator.Tests/MockFillerSourceGeneratorTests.cs b/TestsHelper.SourceGenerator.Tests/MockFillerSourceGeneratorTests.cs index 703438a..9dc221c 100644 --- a/TestsHelper.SourceGenerator.Tests/MockFillerSourceGeneratorTests.cs +++ b/TestsHelper.SourceGenerator.Tests/MockFillerSourceGeneratorTests.cs @@ -113,7 +113,7 @@ public async Task TestHappyFlow_OnValidClassWithGenerateMockWrappersAttribute_Ge } [Test] - public async Task ClassNotPartial_DoNotGenerate_ReportError() + public async Task ClassNotPartial_DoNotGenerate() { // Arrange var test = new Test { @@ -128,10 +128,7 @@ public async Task ClassNotPartial_DoNotGenerate_ReportError() CreateSource("Sources/TestedClass.cs"), }, GeneratedSources = { }, - ExpectedDiagnostics = { - (DiagnosticResult.CompilerError(DiagnosticRegistry.ClassIsNotPartial.Id) - .WithLocation("ATestFixture.cs", 8, 14)) - } + ExpectedDiagnostics = {} } }; // Act + Assert diff --git a/TestsHelper.SourceGenerator/Diagnostics/DiagnosticRegistry.cs b/TestsHelper.SourceGenerator/Diagnostics/DiagnosticRegistry.cs index 4accaa9..6dee405 100644 --- a/TestsHelper.SourceGenerator/Diagnostics/DiagnosticRegistry.cs +++ b/TestsHelper.SourceGenerator/Diagnostics/DiagnosticRegistry.cs @@ -6,15 +6,6 @@ public static class DiagnosticRegistry { private const string Category = "MockFiller"; - public static readonly DiagnosticDescriptor ClassIsNotPartial = new( - id: "TH0001", - title: "Class Is Not Marked Partial", - messageFormat: "Cannot Generate Code For Class {0}. Class Not Mark As Partial.", - category: Category, - defaultSeverity: DiagnosticSeverity.Error, - isEnabledByDefault: true - ); - public static readonly DiagnosticDescriptor MoreThanOneFillMockUsage = new( id: "TH0002", title: "FillMocks Can Be Used Once In Class", diff --git a/TestsHelper.SourceGenerator/MockFilling/TestClassMockCandidateFactory.cs b/TestsHelper.SourceGenerator/MockFilling/TestClassMockCandidateFactory.cs index 80d8949..159da46 100644 --- a/TestsHelper.SourceGenerator/MockFilling/TestClassMockCandidateFactory.cs +++ b/TestsHelper.SourceGenerator/MockFilling/TestClassMockCandidateFactory.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.Diagnostics; +using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -29,15 +30,7 @@ public bool TryCreate(ClassDeclarationSyntax containingClassSyntax, SemanticMode throw new DiagnosticException(DiagnosticRegistry.MoreThanOneFillMockUsage, containingClassSyntax.Identifier.GetLocation()); } - // Error If Class Is Not Partial - if (!containingClassSyntax.Modifiers.Any(SyntaxKind.PartialKeyword)) - { - throw new DiagnosticException( - DiagnosticRegistry.ClassIsNotPartial, - containingClassSyntax.Identifier.GetLocation(), - containingClassSyntax.Identifier.Text - ); - } + Debug.Assert(containingClassSyntax.Modifiers.Any(SyntaxKind.PartialKeyword)); var pair = membersToAttributes.First(); MemberDeclarationSyntax testedClassMember = pair.Key; diff --git a/TestsHelper.SourceGenerator/SourceGeneratorImplementations/IncrementalMockFillerSourceGenerator.cs b/TestsHelper.SourceGenerator/SourceGeneratorImplementations/IncrementalMockFillerSourceGenerator.cs index 76e90ef..fb28dda 100644 --- a/TestsHelper.SourceGenerator/SourceGeneratorImplementations/IncrementalMockFillerSourceGenerator.cs +++ b/TestsHelper.SourceGenerator/SourceGeneratorImplementations/IncrementalMockFillerSourceGenerator.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using TestsHelper.SourceGenerator.Diagnostics; using TestsHelper.SourceGenerator.Diagnostics.Exceptions; @@ -30,7 +31,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) private static bool Predicate(SyntaxNode node, CancellationToken cancellationToken) { - return node is ClassDeclarationSyntax; + return node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.Modifiers.Any(SyntaxKind.PartialKeyword); } private static ResultClass Transform(GeneratorSyntaxContext context, CancellationToken token) From 7b128aa49c90d6ed5e11162fcc1c183e9198e5ed Mon Sep 17 00:00:00 2001 From: Yarin Omesi Date: Fri, 4 Aug 2023 18:15:08 +0300 Subject: [PATCH 3/4] remove Diagnostic reports from transform * replace ContainingClassSyntax with name and namespace --- .../MockFilling/MockFillerImplementation.cs | 30 ++++++++-- .../Models/AttributedTestClassMember.cs | 5 ++ .../Models/TestClassMockCandidate.cs | 16 +++-- .../PartialClassCreator.cs | 9 +-- .../TestClassMockCandidateFactory.cs | 39 ++++++------ .../IncrementalMockFillerSourceGenerator.cs | 59 ++++++------------- 6 files changed, 79 insertions(+), 79 deletions(-) create mode 100644 TestsHelper.SourceGenerator/MockFilling/Models/AttributedTestClassMember.cs diff --git a/TestsHelper.SourceGenerator/MockFilling/MockFillerImplementation.cs b/TestsHelper.SourceGenerator/MockFilling/MockFillerImplementation.cs index 9444fb0..3ace88f 100644 --- a/TestsHelper.SourceGenerator/MockFilling/MockFillerImplementation.cs +++ b/TestsHelper.SourceGenerator/MockFilling/MockFillerImplementation.cs @@ -18,16 +18,22 @@ namespace TestsHelper.SourceGenerator.MockFilling; public class MockFillerImplementation { + private static readonly List EmptyResult = new List(0); + public IReadOnlyList Generate(TestClassMockCandidate testClassMockCandidate) { - ImmutableList constructors = testClassMockCandidate.TestedClassMember + if (!TryExtractOneTestedClassMember(testClassMockCandidate, out AttributedTestClassMember member)) + { + return EmptyResult; + } + + ImmutableList constructors = member.Symbol .GetMembers() .Where(symbol => symbol.Kind == SymbolKind.Method) .OfType() .Where(methodSymbol => methodSymbol.MethodKind == MethodKind.Constructor) .ToImmutableList(); - // TODO: make this smarter IMethodSymbol selectedConstructor = constructors[0]; @@ -49,8 +55,9 @@ public IReadOnlyList Generate(TestClassMockCandidate testClassMockCa List fileBuilders = PartialClassCreator.Create( dependencyBehaviors, - testClassMockCandidate.ContainingClassSyntax, - testClassMockCandidate.GenerateMockWrappers ? WrapperGenerationMode.MethodsWrap : WrapperGenerationMode.OnlyMockWrap, + testClassMockCandidate.ContainingClassIdentifier.Text, + testClassMockCandidate.ContainsClassNamespace, + member.GenerateMockWrapper ? WrapperGenerationMode.MethodsWrap : WrapperGenerationMode.OnlyMockWrap, selectedConstructor, selectedConstructor.ContainingType.Type() ); @@ -58,6 +65,21 @@ public IReadOnlyList Generate(TestClassMockCandidate testClassMockCa return GetFilesResults(fileBuilders).ToList(); } + private static bool TryExtractOneTestedClassMember(TestClassMockCandidate candidate, out AttributedTestClassMember member) + { + switch (candidate.AttributedTestClassMembers.Length) + { + case 0: + member = default; + return false; + case 1: + member = candidate.AttributedTestClassMembers[0]; + return true; + default: + throw new DiagnosticException(DiagnosticRegistry.MoreThanOneFillMockUsage,candidate.ContainingClassIdentifier.GetLocation()); + } + } + [Pure] private static IEnumerable GetFilesResults(IEnumerable fileBuilders) { diff --git a/TestsHelper.SourceGenerator/MockFilling/Models/AttributedTestClassMember.cs b/TestsHelper.SourceGenerator/MockFilling/Models/AttributedTestClassMember.cs new file mode 100644 index 0000000..b308bb6 --- /dev/null +++ b/TestsHelper.SourceGenerator/MockFilling/Models/AttributedTestClassMember.cs @@ -0,0 +1,5 @@ +using Microsoft.CodeAnalysis; + +namespace TestsHelper.SourceGenerator.MockFilling.Models; + +public readonly record struct AttributedTestClassMember(ITypeSymbol Symbol, bool GenerateMockWrapper); \ No newline at end of file diff --git a/TestsHelper.SourceGenerator/MockFilling/Models/TestClassMockCandidate.cs b/TestsHelper.SourceGenerator/MockFilling/Models/TestClassMockCandidate.cs index dbbb1c6..a8f8f10 100644 --- a/TestsHelper.SourceGenerator/MockFilling/Models/TestClassMockCandidate.cs +++ b/TestsHelper.SourceGenerator/MockFilling/Models/TestClassMockCandidate.cs @@ -1,11 +1,17 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; namespace TestsHelper.SourceGenerator.MockFilling.Models; +/// +/// +/// +/// empty when is in global namespace +/// +/// public readonly record struct TestClassMockCandidate( - ClassDeclarationSyntax ContainingClassSyntax, + SyntaxToken ContainingClassIdentifier, + string ContainsClassNamespace, INamedTypeSymbol ContainingClassSymbol, - ITypeSymbol TestedClassMember, - bool GenerateMockWrappers + ImmutableArray AttributedTestClassMembers ); \ No newline at end of file diff --git a/TestsHelper.SourceGenerator/MockFilling/PartialImplementation/PartialClassCreator.cs b/TestsHelper.SourceGenerator/MockFilling/PartialImplementation/PartialClassCreator.cs index 85e5063..a23a9ed 100644 --- a/TestsHelper.SourceGenerator/MockFilling/PartialImplementation/PartialClassCreator.cs +++ b/TestsHelper.SourceGenerator/MockFilling/PartialImplementation/PartialClassCreator.cs @@ -19,7 +19,8 @@ public static class PartialClassCreator public static List Create( Dictionary dependencyBehaviors, - ClassDeclarationSyntax containingClassSyntax, + string containingClassName, + string containingNamespace, WrapperGenerationMode generationMode, IMethodSymbol selectedTestedClassConstructor, IType testedClassType @@ -29,14 +30,10 @@ IType testedClassType List fileBuilders = new(); - string containingClassName = containingClassSyntax.Identifier.Text; - var partialClassFile = FileBuilder.Create($"{containingClassName}.FilledMock.generated.cs"); fileBuilders.Add(partialClassFile); - partialClassFile.Namespace = containingClassSyntax.Parent is BaseNamespaceDeclarationSyntax parentNamespace - ? parentNamespace.Name.ToString() - : string.Empty; + partialClassFile.Namespace = containingNamespace; TypeBuilder partialClassBuilder = partialClassFile.AddClass(name: containingClassName) .Public().Partial(); diff --git a/TestsHelper.SourceGenerator/MockFilling/TestClassMockCandidateFactory.cs b/TestsHelper.SourceGenerator/MockFilling/TestClassMockCandidateFactory.cs index 159da46..8e91272 100644 --- a/TestsHelper.SourceGenerator/MockFilling/TestClassMockCandidateFactory.cs +++ b/TestsHelper.SourceGenerator/MockFilling/TestClassMockCandidateFactory.cs @@ -1,11 +1,10 @@ -using System.Diagnostics; +using System.Collections.Immutable; +using System.Diagnostics; using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using TestsHelper.SourceGenerator.Attributes; -using TestsHelper.SourceGenerator.Diagnostics; -using TestsHelper.SourceGenerator.Diagnostics.Exceptions; using TestsHelper.SourceGenerator.MockFilling.Models; namespace TestsHelper.SourceGenerator.MockFilling; @@ -14,36 +13,32 @@ public class TestClassMockCandidateFactory { private const string MockWrappersAttributeFullName = "TestsHelper.SourceGenerator.MockWrapping.FillMocksWithWrappersAttribute"; - public bool TryCreate(ClassDeclarationSyntax containingClassSyntax, SemanticModel model, out TestClassMockCandidate testClassMockCandidate) + public TestClassMockCandidate Create(ClassDeclarationSyntax containingClassSyntax, SemanticModel model) { string[] attributes = {MockWrappersAttributeFullName, typeof(FillMocksAttribute).FullName}; var membersToAttributes = containingClassSyntax.MembersWithAttribute(model, attributes); - // Check That Only One Member Have The Attribute - switch (membersToAttributes.Count) - { - case 0: - testClassMockCandidate = default; - return false; - case > 1: - throw new DiagnosticException(DiagnosticRegistry.MoreThanOneFillMockUsage, containingClassSyntax.Identifier.GetLocation()); - } - Debug.Assert(containingClassSyntax.Modifiers.Any(SyntaxKind.PartialKeyword)); - var pair = membersToAttributes.First(); - MemberDeclarationSyntax testedClassMember = pair.Key; - bool generateMockWrappers = pair.Value.Contains(MockWrappersAttributeFullName); + ImmutableArray attributedTestClassMembers = membersToAttributes.Select(pair => + { + MemberDeclarationSyntax testedClassMember = pair.Key; + bool generateMockWrappers = pair.Value.Contains(MockWrappersAttributeFullName); + TypeSyntax type = GetTypeFromFieldOrProperty(testedClassMember)!; + ITypeSymbol testedClassTypeSymbol = model.GetTypeInfo(type).Type!; - TypeSyntax type = GetTypeFromFieldOrProperty(testedClassMember)!; + return new AttributedTestClassMember(testedClassTypeSymbol, generateMockWrappers); + }).ToImmutableArray(); - ITypeSymbol testedClassTypeSymbol = model.GetTypeInfo(type).Type!; INamedTypeSymbol declarationSymbol = model.GetDeclaredSymbol(containingClassSyntax)!; - // TODO: diagnostic if there are null - testClassMockCandidate = new TestClassMockCandidate(containingClassSyntax, declarationSymbol, testedClassTypeSymbol, generateMockWrappers); - return true; + return new TestClassMockCandidate( + containingClassSyntax.Identifier, + declarationSymbol.GetNamespace(), + declarationSymbol, + attributedTestClassMembers + ); } private static TypeSyntax? GetTypeFromFieldOrProperty(MemberDeclarationSyntax member) diff --git a/TestsHelper.SourceGenerator/SourceGeneratorImplementations/IncrementalMockFillerSourceGenerator.cs b/TestsHelper.SourceGenerator/SourceGeneratorImplementations/IncrementalMockFillerSourceGenerator.cs index fb28dda..126fe4f 100644 --- a/TestsHelper.SourceGenerator/SourceGeneratorImplementations/IncrementalMockFillerSourceGenerator.cs +++ b/TestsHelper.SourceGenerator/SourceGeneratorImplementations/IncrementalMockFillerSourceGenerator.cs @@ -22,9 +22,9 @@ public class IncrementalMockFillerSourceGenerator : IIncrementalGenerator public void Initialize(IncrementalGeneratorInitializationContext context) { - IncrementalValuesProvider syntaxProvider = context.SyntaxProvider + IncrementalValuesProvider syntaxProvider = context.SyntaxProvider .CreateSyntaxProvider(Predicate, Transform) - .WithComparer(ResultClassEqualityComparer.Instance); + .WithComparer(TestClassMockCandidateEqualityComparer.Instance); context.RegisterSourceOutput(syntaxProvider.Collect(), Execute); } @@ -34,50 +34,23 @@ private static bool Predicate(SyntaxNode node, CancellationToken cancellationTok return node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.Modifiers.Any(SyntaxKind.PartialKeyword); } - private static ResultClass Transform(GeneratorSyntaxContext context, CancellationToken token) + private static TestClassMockCandidate Transform(GeneratorSyntaxContext context, CancellationToken token) { ClassDeclarationSyntax declarationSyntax = (ClassDeclarationSyntax) context.Node; - var reporter = new MemoryDiagnosticReporter(); - using IDisposable _ = GlobalDiagnosticReporter.SetReporterForScope(reporter); - try - { - if (TestClassMockCandidateFactory.TryCreate(declarationSyntax, context.SemanticModel, out TestClassMockCandidate classToFillMockIn)) - { - return new ResultClass(reporter.Diagnostics, classToFillMockIn); - } - } - catch (DiagnosticException e) - { - reporter.Report(e.Diagnostic); - return new ResultClass(reporter.Diagnostics); - } - catch (MultipleDiagnosticsException e) - { - reporter.ReportMultiple(e.Diagnostics); - return new ResultClass(reporter.Diagnostics); - } - - return new ResultClass(reporter.Diagnostics); + return TestClassMockCandidateFactory.Create(declarationSyntax, context.SemanticModel); } - private static void Execute(SourceProductionContext context, ImmutableArray classesToFill) + private static void Execute(SourceProductionContext context, ImmutableArray classMockCandidates) { var reporter = new ActionDiagnosticReporter(context.ReportDiagnostic); using IDisposable _ = GlobalDiagnosticReporter.SetReporterForScope(reporter); - foreach (ResultClass resultClass in classesToFill) + foreach (TestClassMockCandidate testClassMockCandidate in classMockCandidates) { - // Report All Diagnostics From First Phase - reporter.ReportMultiple(resultClass.Diagnostics); - - // Continue if there is no class to fill mock in - if(resultClass.ClassToFillMockIn is not { } classToFillMockIn) - continue; - ReportCatcher.RunCode(() => { - foreach (FileResult result in MockFillerImplementation.Generate(classToFillMockIn)) + foreach (FileResult result in MockFillerImplementation.Generate(testClassMockCandidate)) { context.AddSource(result.FileName, result.SourceCode); } @@ -85,16 +58,18 @@ private static void Execute(SourceProductionContext context, ImmutableArray Diagnostics, TestClassMockCandidate? ClassToFillMockIn = null); - - private sealed class ResultClassEqualityComparer : IEqualityComparer + private sealed class TestClassMockCandidateEqualityComparer : IEqualityComparer { - public static ResultClassEqualityComparer Instance { get; } = new ResultClassEqualityComparer(); - public bool Equals(ResultClass x, ResultClass y) + public static TestClassMockCandidateEqualityComparer Instance { get; } = new TestClassMockCandidateEqualityComparer(); + + public bool Equals(TestClassMockCandidate x, TestClassMockCandidate y) { - return x.Diagnostics.SequenceEqual(y.Diagnostics) && - Nullable.Equals(x.ClassToFillMockIn, y.ClassToFillMockIn); + return x.ContainingClassIdentifier.Equals(y.ContainingClassIdentifier) + && x.ContainsClassNamespace == y.ContainsClassNamespace + && SymbolEqualityComparer.Default.Equals(x.ContainingClassSymbol, y.ContainingClassSymbol) + && x.AttributedTestClassMembers.SequenceEqual(y.AttributedTestClassMembers); } - public int GetHashCode(ResultClass obj) => throw new NotImplementedException(); + + public int GetHashCode(TestClassMockCandidate obj) => throw new NotImplementedException(); } } \ No newline at end of file From b18f77a53f5042ebec3707b64d602ef16a333454 Mon Sep 17 00:00:00 2001 From: Yarin Omesi Date: Fri, 4 Aug 2023 18:21:12 +0300 Subject: [PATCH 4/4] Remove unused MemoryDiagnosticReporter --- .../Reporters/MemoryDiagnosticReporter.cs | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 TestsHelper.SourceGenerator/Diagnostics/Reporters/MemoryDiagnosticReporter.cs diff --git a/TestsHelper.SourceGenerator/Diagnostics/Reporters/MemoryDiagnosticReporter.cs b/TestsHelper.SourceGenerator/Diagnostics/Reporters/MemoryDiagnosticReporter.cs deleted file mode 100644 index 1605b1d..0000000 --- a/TestsHelper.SourceGenerator/Diagnostics/Reporters/MemoryDiagnosticReporter.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using Microsoft.CodeAnalysis; - -namespace TestsHelper.SourceGenerator.Diagnostics.Reporters; - -public class MemoryDiagnosticReporter : IDiagnosticReporter -{ - public IReadOnlyList Diagnostics => _diagnostics; - - private readonly List _diagnostics; - - public MemoryDiagnosticReporter() - { - _diagnostics = new List(); - } - - public void Report(Diagnostic diagnostic) => _diagnostics.Add(diagnostic); -} \ No newline at end of file