8000 feat: add incremental generator tests by TimothyMakkison · Pull Request #1829 · reactiveui/refit · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat: add incremental generator tests #1829

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

Merged
merged 2 commits into from
Sep 24, 2024
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
6 changes: 6 additions & 0 deletions InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
/// <param name="candidateMethods">The candidate methods.</param>
/// <param name="candidateInterfaces">The candidate interfaces.</param>
/// <returns></returns>
public void GenerateInterfaceStubs<TContext>(

Check warning on line 70 in InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs

View workflow job for this annotation

GitHub Actions / build / build

Member 'GenerateInterfaceStubs' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 70 in InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs

View workflow job for this annotation

GitHub Actions / build / build

Member 'GenerateInterfaceStubs' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)
TContext context,
Action<TContext, Diagnostic> reportDiagnostic,
Action<TContext, string, SourceText> addSource,
Expand All @@ -82,7 +82,7 @@

// we're going to create a new compilation that contains the attribute.
// TODO: we should allow source generators to provide source during initialize, so that this step isn't required.
var options = (CSharpParseOptions)compilation.SyntaxTrees[0].Options;

Check warning on line 85 in InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs

View workflow job for this annotation

GitHub Actions / build / build

In externally visible method 'void InterfaceStubGenerator.GenerateInterfaceStubs<TContext>(TContext context, Action<TContext, Diagnostic> reportDiagnostic, Action<TContext, string, SourceText> addSource, CSharpCompilation compilation, string? refitInternalNamespace, ImmutableArray<MethodDeclarationSyntax> candidateMethods, ImmutableArray<InterfaceDeclarationSyntax> candidateInterfaces)', validate parameter 'compilation' is non-null before using it. If appropriate, throw an 'ArgumentNullException' when the argument is 'null'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1062)

Check warning on line 85 in InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs

View workflow job for this annotation

GitHub Actions / build / build

In externally visible method 'void InterfaceStubGeneratorV2.GenerateInterfaceStubs<TContext>(TContext context, Action<TContext, Diagnostic> reportDiagnostic, Action<TContext, string, SourceText> addSource, CSharpCompilation compilation, string? refitInternalNamespace, ImmutableArray<MethodDeclarationSyntax> candidateMethods, ImmutableArray<InterfaceDeclarationSyntax> candidateInterfaces)', validate parameter 'compilation' is non-null before using it. If appropriate, throw an 'ArgumentNullException' when the argument is 'null'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1062)

var disposableInterfaceSymbol = compilation.GetTypeByMetadataName(
"System.IDisposable"
Expand All @@ -93,7 +93,7 @@

if (httpMethodBaseAttributeSymbol == null)
{
reportDiagnostic(context, Diagnostic.Create(DiagnosticDescriptors.RefitNotReferenced, null));

Check warning on line 96 in InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs

View workflow job for this annotation

GitHub Actions / build / build

In externally visible method 'void InterfaceStubGenerator.GenerateInterfaceStubs<TContext>(TContext context, Action<TContext, Diagnostic> reportDiagnostic, Action<TContext, string, SourceText> addSource, CSharpCompilation compilation, string? refitInternalNamespace, ImmutableArray<MethodDeclarationSyntax> candidateMethods, ImmutableArray<InterfaceDeclarationSyntax> candidateInterfaces)', validate parameter 'reportDiagnostic' is non-null before using it. If appropriate, throw an 'ArgumentNullException' when the argument is 'null'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1062)

Check warning on line 96 in InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs

View workflow job for this annotation

GitHub Actions / build / build

In externally visible method 'void InterfaceStubGeneratorV2.GenerateInterfaceStubs<TContext>(TContext context, Action<TContext, Diagnostic> reportDiagnostic, Action<TContext, string, SourceText> addSource, CSharpCompilation compilation, string? refitInternalNamespace, ImmutableArray<MethodDeclarationSyntax> candidateMethods, ImmutableArray<InterfaceDeclarationSyntax> candidateInterfaces)', validate parameter 'reportDiagnostic' is non-null before using it. If appropriate, throw an 'ArgumentNullException' when the argument is 'null'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1062)
return;
}

Expand Down Expand Up @@ -199,7 +199,7 @@
);

// add the attribute text
addSource(

Check warning on line 202 in InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs

View workflow job for this annotation

GitHub Actions / build / build

In externally visible method 'void InterfaceStubGenerator.GenerateInterfaceStubs<TContext>(TContext context, Action<TContext, Diagnostic> reportDiagnostic, Action<TContext, string, SourceText> addSource, CSharpCompilation compilation, string? refitInternalNamespace, ImmutableArray<MethodDeclarationSyntax> candidateMethods, ImmutableArray<InterfaceDeclarationSyntax> candidateInterfaces)', validate parameter 'addSource' is non-null before using it. If appropriate, throw an 'ArgumentNullException' when the argument is 'null'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1062)

Check warning on line 202 in InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs

View workflow job for this annotation

GitHub Actions / build / build

In externally visible method 'void InterfaceStubGeneratorV2.GenerateInterfaceStubs<TContext>(TContext context, Action<TContext, Diagnostic> reportDiagnostic, Action<TContext, string, SourceText> addSource, CSharpCompilation compilation, string? refitInternalNamespace, ImmutableArray<MethodDeclarationSyntax> candidateMethods, ImmutableArray<InterfaceDeclarationSyntax> candidateInterfaces)', validate parameter 'addSource' is non-null before using it. If appropriate, throw an 'ArgumentNullException' when the argument is 'null'. (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1062)
context,
"PreserveAttribute.g.cs",
SourceText.From(attributeText, Encoding.UTF8)
Expand Down Expand Up @@ -846,7 +846,7 @@
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
}

class SyntaxReceiver : ISyntaxReceiver

Check warning on line 849 in InterfaceStubGenerator.Shared/InterfaceStubGenerator.cs

View workflow job for this annotation

GitHub Actions / build / build

Type 'SyntaxReceiver' can be sealed because it has no subtypes in its containing assembly and is not externally visible (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1852)
{
public List<MethodDeclarationSyntax> CandidateMethods { get; } = new();

Expand Down Expand Up @@ -875,4 +875,10 @@

#endif
}

internal static class RefitGeneratorStepName
{
public const string ReportDiagnostics = "ReportDiagnostics";
public const string BuildRefit = "BuildRefit";
}
}
9 changes: 7 additions & 2 deletions Refit.GeneratorTests/Fixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public static Task VerifyForDeclaration(string declarations)
return VerifyGenerator(source);
}

private static CSharpCompilation CreateLibrary(params string[] source)
public static CSharpCompilation CreateLibrary(params SyntaxTree[] source)
{
var references = new List<MetadataReference>();
var assemblies = AssemblyReferencesForCodegen;
Expand All @@ -112,14 +112,19 @@ private static CSharpCompilation CreateLibrary(params string[] source)
references.Add(RefitAssembly);
var compilation = CSharpCompilation.Create(
"compilation",
source.Select(s => CSharpSyntaxTree.ParseText(s)),
source,
references,
new CSharpCompilationOptions(OutputKind.ConsoleApplication)
);

return compilation;
}

private static CSharpCompilation CreateLibrary(params string[] source)
{
return CreateLibrary(source.Select(s => CSharpSyntaxTree.ParseText(s)).ToArray());
}

private static Task<VerifyResult> VerifyGenerator(string source, bool ignoreNonInterfaces = true)
{
var compilation = CreateLibrary(source);
Expand Down
31 changes: 31 additions & 0 deletions Refit.GeneratorTests/Incremental/IncrementalGeneratorRunReasons.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Microsoft.CodeAnalysis;

namespace Refit.GeneratorTests.Incremental;

internal record IncrementalGeneratorRunReasons(
IncrementalStepRunReason BuildMediatorStep,
IncrementalStepRunReason ReportDiagnosticsStep
)
{
public static readonly IncrementalGeneratorRunReasons New =
new(IncrementalStepRunReason.New, IncrementalStepRunReason.New);

public static readonly IncrementalGeneratorRunReasons Cached =
new(
// compilation step should always be modified as each time a new compilation is passed
IncrementalStepRunReason.Cached,
IncrementalStepRunReason.Cached
);

public static readonly IncrementalGeneratorRunReasons Modified = Cached with
{
ReportDiagnosticsStep = IncrementalStepRunReason.Modified,
BuildMediatorStep = IncrementalStepRunReason.Modified,
};

public static readonly IncrementalGeneratorRunReasons ModifiedSource = Cached with
{
ReportDiagnosticsStep = IncrementalStepRunReason.Unchanged,
BuildMediatorStep = IncrementalStepRunReason.Modified,
};
}
211 changes: 211 additions & 0 deletions Refit.GeneratorTests/Incremental/IncrementalTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
using Microsoft.CodeAnalysis.CSharp;

namespace Refit.GeneratorTests.Incremental;

public class IncrementalTest
{
private const string Default =
"""
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Refit;

namespace RefitGeneratorTest;

public interface IGitHubApi
{
[Get("/users/{user}")]
Task<string> GetUser(string user);
}
""";

// [Fact]
public void AddUnrelatedTypeDoesntRegenerate()
{
var syntaxTree = CSharpSyntaxTree.ParseText(Default, CSharpParseOptions.Default);
var compilation1 = Fixture.CreateLibrary(syntaxTree);

var driver1 = TestHelper.GenerateTracked(compilation1);
TestHelper.AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New);

var compilation2 = compilation1.AddSyntaxTrees(CSharpSyntaxTree.ParseText("struct MyValue {}"));
var driver2 = driver1.RunGenerators(compilation2);
TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached);
}

// [Fact]
public void SmallChangeDoesntRegenerate()
{
var syntaxTree = CSharpSyntaxTree.ParseText(Default, CSharpParseOptions.Default);
var compilation1 = Fixture.CreateLibrary(syntaxTree);

var driver1 = TestHelper.GenerateTracked(compilation1);
TestHelper.AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New);

// only change body, don't change the method
var compilation2 = TestHelper.ReplaceMemberDeclaration(
compilation1,
"IGitHubApi",
"""
public interface IGitHubApi
{
[Get("/users/{user}")]
Task<string> GetUser(string user);

private record Temp();
}
"""
);
var driver2 = driver1.RunGenerators(compilation2);
TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.Cached);
}

// [Fact]
public void ModifyParameterNameDoesRegenerate()
{
var syntaxTree = CSharpSyntaxTree.ParseText(Default, CSharpParseOptions.Default);
var compilation1 = Fixture.CreateLibrary(syntaxTree);

var driver1 = TestHelper.GenerateTracked(compilation1);
TestHelper.AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New);

// change parameter name
var newInterface =
"""
public interface IGitHubApi
{
[Get("/users/{myUser}")]
Task<string> GetUser(string myUser);
}
""";
var compilation2 = TestHelper.ReplaceMemberDeclaration(compilation1, "IGitHubApi", newInterface);

var driver2 = driver1.RunGenerators(compilation2);
TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.ModifiedSource);
}

// [Fact]
public void ModifyParameterTypeDoesRegenerate()
{
var syntaxTree = CSharpSyntaxTree.ParseText(Default, CSharpParseOptions.Default);
var compilation1 = Fixture.CreateLibrary(syntaxTree);

var driver1 = TestHelper.GenerateTracked(compilation1);
TestHelper.AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New);

// change parameter type
var newInterface =
"""
public interface IGitHubApi
{
[Get("/users/{user}")]
Task<string> GetUser(int user);
}
""";
var compilation2 = TestHelper.ReplaceMemberDeclaration(compilation1, "IGitHubApi", newInterface);

var driver2 = driver1.RunGenerators(compilation2);
TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.ModifiedSource);
}

// [Fact]
public void ModifyParameterNullabilityDoesRegenerate()
{
var syntaxTree = CSharpSyntaxTree.ParseText(Default, CSharpParseOptions.Default);
var compilation1 = Fixture.CreateLibrary(syntaxTree);

var driver1 = TestHelper.GenerateTracked(compilation1);
TestHelper.AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New);

// change parameter nullability
var newInterface =
"""
public interface IGitHubApi
{
[Get("/users/{user}")]
Task<string> GetUser(string? user);
}
""";
var compilation2 = TestHelper.ReplaceMemberDeclaration(compilation1, "IGitHubApi", newInterface);

var driver2 = driver1.RunGenerators(compilation2);
TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.ModifiedSource);
}

// [Fact]
public void AddParameterDoesRegenerate()
{
var syntaxTree = CSharpSyntaxTree.ParseText(Default, CSharpParseOptions.Default);
var compilation1 = Fixture.CreateLibrary(syntaxTree);

var driver1 = TestHelper.GenerateTracked(compilation1);
TestHelper.AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New);

// add parameter
var newInterface =
"""
public interface IGitHubApi
{
[Get("/users/{user}")]
Task<string> GetUser(string user, [Query] int myParam);
}
""";
var compilation2 = TestHelper.ReplaceMemberDeclaration(compilation1, "IGitHubApi", newInterface);

var driver2 = driver1.RunGenerators(compilation2);
TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.ModifiedSource);
}

// [Fact]
public void ModifyReturnTypeDoesRegenerate()
{
var syntaxTree = CSharpSyntaxTree.ParseText(Default, CSharpParseOptions.Default);
var compilation1 = Fixture.CreateLibrary(syntaxTree);

var driver1 = TestHelper.GenerateTracked(compilation1);
TestHelper.AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New);

// change return type
var newInterface =
"""
public interface IGitHubApi
{
[Get("/users/{user}")]
Task<int> GetUser(string user);
}
""";
var compilation2 = TestHelper.ReplaceMemberDeclaration(compilation1, "IGitHubApi", newInterface);

var driver2 = driver1.RunGenerators(compilation2);
TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.ModifiedSource);
}

// [Fact]
public void ModifyReturnNullabilityDoesRegenerate()
{
var syntaxTree = CSharpSyntaxTree.ParseText(Default, CSharpParseOptions.Default);
var compilation1 = Fixture.CreateLibrary(syntaxTree);

var driver1 = TestHelper.GenerateTracked(compilation1);
TestHelper.AssertRunReasons(driver1, IncrementalGeneratorRunReasons.New);

// change return nullability
var newInterface =
"""
public interface IGitHubApi
{
[Get("/users/{user}")]
Task<string?> GetUser(string user);
}
""";
var compilation2 = TestHelper.ReplaceMemberDeclaration(compilation1, "IGitHubApi", newInterface);

var driver2 = driver1.RunGenerators(compilation2);
TestHelper.AssertRunReasons(driver2, IncrementalGeneratorRunReasons.ModifiedSource);
}
}
103 changes: 103 additions & 0 deletions Refit.GeneratorTests/Incremental/TestHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

using Refit.Generator;

namespace Refit.GeneratorTests.Incremental;

internal static class TestHelper
{
private static readonly GeneratorDriverOptions EnableIncrementalTrackingDriverOptions =
new(IncrementalGeneratorOutputKind.None, trackIncrementalGeneratorSteps: true);

internal static GeneratorDriver GenerateTracked(Compilation compilation)
{
var generator = new InterfaceStubGeneratorV2();

var driver = CSharpGeneratorDriver.Create(
new[] { generator.AsSourceGenerator() },
driverOptions: EnableIncrementalTrackingDriverOptions
);
return driver.RunGenerators(compilation);
}

internal static CSharpCompilation ReplaceMemberDeclaration(
CSharpCompilation compilation,
string memberName,
string newMember
)
{
var syntaxTree = compilation.SyntaxTrees.Single();
var memberDeclaration = syntaxTree
.GetCompilationUnitRoot()
.DescendantNodes()
.OfType<TypeDeclarationSyntax>()
.Single(x => x.Identifier.Text == memberName);
var updatedMemberDeclaration = SyntaxFactory.ParseMemberDeclaration(newMember)!;

var newRoot = syntaxTree.GetCompilationUnitRoot().ReplaceNode(memberDeclaration, updatedMemberDeclaration);
var newTree = syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options);

return compilation.ReplaceSyntaxTree(compilation.SyntaxTrees.First(), newTree);
}

internal static CSharpCompilation ReplaceLocalDeclaration(
CSharpCompilation compilation,
string variableName,
string newDeclaration
)
{
var syntaxTree = compilation.SyntaxTrees.Single();

var memberDeclaration = syntaxTree
.GetCompilationUnitRoot()
.DescendantNodes()
.OfType<LocalDeclarationStatementSyntax>()
.Single(x => x.Declaration.Variables.Any(x => x.Identifier.ToString() == variableName));
var updatedMemberDeclaration = SyntaxFactory.ParseStatement(newDeclaration)!;

var newRoot = syntaxTree.GetCompilationUnitRoot().ReplaceNode(memberDeclaration, updatedMemberDeclaration);
var newTree = syntaxTree.WithRootAndOptions(newRoot, syntaxTree.Options);

return compilation.ReplaceSyntaxTree(compilation.SyntaxTrees.First(), newTree);
}

internal static void AssertRunReasons(
GeneratorDriver driver,
IncrementalGeneratorRunReasons reasons,
int outputIndex = 0
)
{
var runResult = driver.GetRunResult().Results[0];

AssertRunReason(
runResult,
RefitGeneratorStepName.ReportDiagnostics,
reasons.ReportDiagnosticsStep,
outputIndex
);
AssertRunReason(runResult, RefitGeneratorStepName.BuildRefit, reasons.BuildMediatorStep, outputIndex);
}

private static void AssertRunReason(
GeneratorRunResult runResult,
string stepName,
IncrementalStepRunReason expectedStepReason,
int outputIndex
)
{
var actualStepReason = runResult
.TrackedSteps[stepName]
.SelectMany(x => x.Outputs)
.ElementAt(outputIndex)
.Reason;
Assert.Equal(actualStepReason, expectedStepReason);
}
}

internal static class RefitGeneratorStepName
{
public const string ReportDiagnostics = "ReportDiagnostics";
public const string BuildRefit = "BuildRefit";
}
Loading
0