Skip to content

Commit

Permalink
Improvments to Incremental Generator (#45)
Browse files Browse the repository at this point in the history
* Create ResultClassEqualityComparer and use it
* ignore not partial classes
* Fix test
* Remove `ClassIsNotPartial` diagnostic
* Remove diagnostic report in `TestClassMockCandidateFactory`
* remove Diagnostic reports from transform
* replace ContainingClassSyntax with name and namespace
* Remove unused MemoryDiagnosticReporter
  • Loading branch information
YarinOmesi authored Aug 5, 2023
1 parent ce11b91 commit d6d82d3
Show file tree
Hide file tree
Showing 9 changed files with 90 additions and 113 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<TSourceGenerator> {
Expand All @@ -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
Expand Down
9 changes: 0 additions & 9 deletions TestsHelper.SourceGenerator/Diagnostics/DiagnosticRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,22 @@ namespace TestsHelper.SourceGenerator.MockFilling;

public class MockFillerImplementation
{
private static readonly List<FileResult> EmptyResult = new List<FileResult>(0);

public IReadOnlyList<FileResult> Generate(TestClassMockCandidate testClassMockCandidate)
{
ImmutableList<IMethodSymbol> constructors = testClassMockCandidate.TestedClassMember
if (!TryExtractOneTestedClassMember(testClassMockCandidate, out AttributedTestClassMember member))
{
return EmptyResult;
}

ImmutableList<IMethodSymbol> constructors = member.Symbol
.GetMembers()
.Where(symbol => symbol.Kind == SymbolKind.Method)
.OfType<IMethodSymbol>()
.Where(methodSymbol => methodSymbol.MethodKind == MethodKind.Constructor)
.ToImmutableList();


// TODO: make this smarter
IMethodSymbol selectedConstructor = constructors[0];

Expand All @@ -49,15 +55,31 @@ public IReadOnlyList<FileResult> Generate(TestClassMockCandidate testClassMockCa

List<FileBuilder> 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()
);

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<FileResult> GetFilesResults(IEnumerable<FileBuilder> fileBuilders)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Microsoft.CodeAnalysis;

namespace TestsHelper.SourceGenerator.MockFilling.Models;

public readonly record struct AttributedTestClassMember(ITypeSymbol Symbol, bool GenerateMockWrapper);
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;

namespace TestsHelper.SourceGenerator.MockFilling.Models;

/// <summary>
///
/// </summary>
/// <param name="ContainsClassNamespace">empty when is in global namespace</param>
/// <param name="ContainingClassSymbol"></param>
/// <param name="AttributedTestClassMembers"></param>
public readonly record struct TestClassMockCandidate(
ClassDeclarationSyntax ContainingClassSyntax,
SyntaxToken ContainingClassIdentifier,
string ContainsClassNamespace,
INamedTypeSymbol ContainingClassSymbol,
ITypeSymbol TestedClassMember,
bool GenerateMockWrappers
ImmutableArray<AttributedTestClassMember> AttributedTestClassMembers
);
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public static class PartialClassCreator

public static List<FileBuilder> Create(
Dictionary<string, IDependencyInitializationBehavior> dependencyBehaviors,
ClassDeclarationSyntax containingClassSyntax,
string containingClassName,
string containingNamespace,
WrapperGenerationMode generationMode,
IMethodSymbol selectedTestedClassConstructor,
IType testedClassType
Expand All @@ -29,14 +30,10 @@ IType testedClassType

List<FileBuilder> 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();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using System.Linq;
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;
Expand All @@ -13,44 +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));

// Error If Class Is Not Partial
if (!containingClassSyntax.Modifiers.Any(SyntaxKind.PartialKeyword))
ImmutableArray<AttributedTestClassMember> attributedTestClassMembers = membersToAttributes.Select(pair =>
{
throw new DiagnosticException(
DiagnosticRegistry.ClassIsNotPartial,
containingClassSyntax.Identifier.GetLocation(),
containingClassSyntax.Identifier.Text
);
}

var pair = membersToAttributes.First();
MemberDeclarationSyntax testedClassMember = pair.Key;
bool generateMockWrappers = pair.Value.Contains(MockWrappersAttributeFullName);
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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
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;
Expand All @@ -20,67 +22,54 @@ public class IncrementalMockFillerSourceGenerator : IIncrementalGenerator

public void Initialize(IncrementalGeneratorInitializationContext context)
{
IncrementalValuesProvider<ResultClass> syntaxProvider = context.SyntaxProvider
.CreateSyntaxProvider(Predicate, Transform);
IncrementalValuesProvider<TestClassMockCandidate> syntaxProvider = context.SyntaxProvider
.CreateSyntaxProvider(Predicate, Transform)
.WithComparer(TestClassMockCandidateEqualityComparer.Instance);

context.RegisterSourceOutput(syntaxProvider.Collect(), Execute);
}

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)
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<ResultClass> classesToFill)
private static void Execute(SourceProductionContext context, ImmutableArray<TestClassMockCandidate> 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);
}
});
}
}

private readonly record struct ResultClass(IReadOnlyList<Diagnostic> Diagnostics, TestClassMockCandidate? ClassToFillMockIn = null);
private sealed class TestClassMockCandidateEqualityComparer : IEqualityComparer<TestClassMockCandidate>
{
public static TestClassMockCandidateEqualityComparer Instance { get; } = new TestClassMockCandidateEqualityComparer();

public bool Equals(TestClassMockCandidate x, TestClassMockCandidate y)
{
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(TestClassMockCandidate obj) => throw new NotImplementedException();
}
}

0 comments on commit d6d82d3

Please sign in to comment.