Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvments to Incremental Generator #45

Merged
merged 4 commits into from
Aug 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
}
}
Loading