Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>
<DevelopmentDependency>false</DevelopmentDependency>
<NoPackageAnalysis>true</NoPackageAnalysis>
<RootNamespace>TUnit.AspNetCore.Analyzers.CodeFixers</RootNamespace>
<AssemblyName>TUnit.AspNetCore.Analyzers.CodeFixers</AssemblyName>
<NoWarn>RS2003</NoWarn>
<EnableTrimAnalyzer>false</EnableTrimAnalyzer>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>TUnit.AspNetCore.Analyzers.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.Common" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.Common" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Remove="AnalyzerReleases.Unshipped.md" />
<AdditionalFiles Remove="AnalyzerReleases.Shipped.md" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TUnit.AspNetCore.Analyzers\TUnit.AspNetCore.Analyzers.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Simplification;
using TUnit.AspNetCore.Analyzers;

namespace TUnit.AspNetCore.Analyzers.CodeFixers;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(UseTestWebApplicationFactoryCodeFixProvider)), Shared]
public class UseTestWebApplicationFactoryCodeFixProvider : CodeFixProvider
{
private const string Title = "Inherit from TestWebApplicationFactory<T>";
private const string TestWebApplicationFactoryName = "TestWebApplicationFactory";
private const string TestWebApplicationFactoryNamespace = "TUnit.AspNetCore";

public sealed override ImmutableArray<string> FixableDiagnosticIds { get; } =
ImmutableArray.Create(Rules.DirectWebApplicationFactoryInheritance.Id);

public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
{
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
if (root is null)
{
return;
}

foreach (var diagnostic in context.Diagnostics)
{
var baseTypeSyntax = root
.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true)
.FirstAncestorOrSelf<BaseTypeSyntax>();

if (baseTypeSyntax is null)
{
continue;
}

context.RegisterCodeFix(
CodeAction.Create(
title: Title,
createChangedDocument: c => ReplaceBaseTypeAsync(context.Document, baseTypeSyntax, c),
equivalenceKey: Title),
diagnostic);
}
}

private static async Task<Document> ReplaceBaseTypeAsync(
Document document,
BaseTypeSyntax baseTypeSyntax,
CancellationToken cancellationToken)
{
var genericName = baseTypeSyntax.Type switch
{
GenericNameSyntax g => g,
QualifiedNameSyntax { Right: GenericNameSyntax q } => q,
AliasQualifiedNameSyntax { Name: GenericNameSyntax a } => a,
_ => null,
};

if (genericName is null)
{
return document;
}

var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
if (root is not CompilationUnitSyntax compilationUnit)
{
return document;
}

var newTypeName = SyntaxFactory.GenericName(SyntaxFactory.Identifier(TestWebApplicationFactoryName))
.WithTypeArgumentList(genericName.TypeArgumentList);

var newBaseType = baseTypeSyntax.WithType(newTypeName)
.WithTriviaFrom(baseTypeSyntax)
.WithAdditionalAnnotations(Simplifier.Annotation, Formatter.Annotation);

var newCompilationUnit = compilationUnit.ReplaceNode(baseTypeSyntax, newBaseType);
newCompilationUnit = AddUsingIfMissing(newCompilationUnit, TestWebApplicationFactoryNamespace);

return document.WithSyntaxRoot(newCompilationUnit);
}

private static CompilationUnitSyntax AddUsingIfMissing(CompilationUnitSyntax compilationUnit, string namespaceName)
{
if (compilationUnit.Usings.Any(u => u.Name?.ToString() == namespaceName))
{
return compilationUnit;
}

var newUsing = SyntaxFactory.UsingDirective(SyntaxFactory.ParseName(namespaceName))
.WithTrailingTrivia(SyntaxFactory.ElasticCarriageReturnLineFeed);

return compilationUnit.AddUsings(newUsing);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Verifier = TUnit.AspNetCore.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier<TUnit.AspNetCore.Analyzers.DirectWebApplicationFactoryInheritanceAnalyzer>;

namespace TUnit.AspNetCore.Analyzers.Tests;

public class DirectWebApplicationFactoryInheritanceAnalyzerTests
{
[Test]
public async Task Warning_When_Direct_WebApplicationFactory_Inheritance()
{
await Verifier.VerifyAnalyzerAsync(
$$"""
{{WebApplicationFactoryStubs.Source}}

public class MyFactory : {|#0:Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<Program>|}
{
}
""",
Verifier.Diagnostic(Rules.DirectWebApplicationFactoryInheritance)
.WithLocation(0)
.WithArguments("MyFactory"));
}

[Test]
public async Task No_Warning_When_Using_TestWebApplicationFactory()
{
await Verifier.VerifyAnalyzerAsync(
$$"""
{{WebApplicationFactoryStubs.Source}}

public class MyFactory : TUnit.AspNetCore.TestWebApplicationFactory<Program>
{
}
""");
}

[Test]
public async Task No_Warning_When_Transitively_Inherits_Via_TestWebApplicationFactory()
{
await Verifier.VerifyAnalyzerAsync(
$$"""
{{WebApplicationFactoryStubs.Source}}

public class BaseFactory : TUnit.AspNetCore.TestWebApplicationFactory<Program>
{
}

public class MyFactory : BaseFactory
{
}
""");
}

[Test]
public async Task Warning_On_Base_Type_Location()
{
await Verifier.VerifyAnalyzerAsync(
$$"""
{{WebApplicationFactoryStubs.Source}}

public class A : {|#0:Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<Program>|} { }
public class B : {|#1:Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<Program>|} { }
""",
Verifier.Diagnostic(Rules.DirectWebApplicationFactoryInheritance).WithLocation(0).WithArguments("A"),
Verifier.Diagnostic(Rules.DirectWebApplicationFactoryInheritance).WithLocation(1).WithArguments("B"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" VersionOverride="4.8.0" />
<PackageReference Include="Microsoft.Testing.Extensions.HangDump" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\TUnit.AspNetCore.Analyzers\TUnit.AspNetCore.Analyzers.csproj" />
<ProjectReference Include="..\TUnit.AspNetCore.Analyzers.CodeFixers\TUnit.AspNetCore.Analyzers.CodeFixers.csproj" />
<ProjectReference Include="..\TUnit.Engine\TUnit.Engine.csproj" />
<ProjectReference Include="..\TUnit.Core\TUnit.Core.csproj" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Verifier = TUnit.AspNetCore.Analyzers.Tests.Verifiers.CSharpCodeFixVerifier<
TUnit.AspNetCore.Analyzers.DirectWebApplicationFactoryInheritanceAnalyzer,
TUnit.AspNetCore.Analyzers.CodeFixers.UseTestWebApplicationFactoryCodeFixProvider>;

namespace TUnit.AspNetCore.Analyzers.Tests;

public class UseTestWebApplicationFactoryCodeFixProviderTests
{
[Test]
public async Task Rewrites_Base_Type_To_TestWebApplicationFactory()
{
var source = $$"""
{{WebApplicationFactoryStubs.Source}}

public class MyFactory : {|#0:Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<Program>|}
{
}
""";

var fixedSource = $$"""
using TUnit.AspNetCore;

{{WebApplicationFactoryStubs.Source}}

public class MyFactory : TestWebApplicationFactory<Program>
{
}
""";

await Verifier.VerifyCodeFixAsync(
source,
fixedSource,
Verifier.Diagnostic(Rules.DirectWebApplicationFactoryInheritance)
.WithLocation(0)
.WithArguments("MyFactory"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Testing;
using Microsoft.CodeAnalysis.Text;

namespace TUnit.AspNetCore.Analyzers.Tests.Verifiers;

public static class CSharpCodeFixVerifier<TAnalyzer, TCodeFix>
where TAnalyzer : DiagnosticAnalyzer, new()
where TCodeFix : CodeFixProvider, new()
{
public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor)
=> CSharpCodeFixVerifier<TAnalyzer, TCodeFix, LineEndingNormalizingVerifier>.Diagnostic(descriptor);

public static async Task VerifyCodeFixAsync(
[StringSyntax("c#")] string source,
[StringSyntax("c#")] string fixedSource,
params DiagnosticResult[] expected)
{
var test = new CSharpCodeFixTest<TAnalyzer, TCodeFix, LineEndingNormalizingVerifier>
{
TestCode = source,
FixedCode = fixedSource,
ReferenceAssemblies = ReferenceAssemblies.Net.Net90,
};

test.TestState.AnalyzerConfigFiles.Add(("/.editorconfig", SourceText.From("""
is_global = true
end_of_line = lf
""")));

test.SolutionTransforms.Add((solution, projectId) =>
{
var project = solution.GetProject(projectId);
if (project?.ParseOptions is not CSharpParseOptions parseOptions)
{
return solution;
}

return solution.WithProjectParseOptions(projectId, parseOptions.WithLanguageVersion(LanguageVersion.Preview));
});

test.ExpectedDiagnostics.AddRange(expected);

await test.RunAsync(CancellationToken.None);
}
}
23 changes: 23 additions & 0 deletions TUnit.AspNetCore.Analyzers.Tests/WebApplicationFactoryStubs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace TUnit.AspNetCore.Analyzers.Tests;

internal static class WebApplicationFactoryStubs
{
public const string Source = """
namespace Microsoft.AspNetCore.Mvc.Testing
{
public class WebApplicationFactory<TEntryPoint> where TEntryPoint : class
{
}
}

namespace TUnit.AspNetCore
{
public class TestWebApplicationFactory<TEntryPoint> : Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>
where TEntryPoint : class
{
}
}

public class Program { }
""";
}
1 change: 1 addition & 0 deletions TUnit.AspNetCore.Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Rule ID | Category | Severity | Notes
--------|----------|----------|-------
TUnit0062 | Usage | Error | Factory property accessed before initialization in WebApplicationTest
TUnit0063 | Usage | Error | GlobalFactory member access breaks test isolation
TUnit0064 | Usage | Warning | Inherit from TestWebApplicationFactory&lt;T&gt; instead of WebApplicationFactory&lt;T&gt;

### Removed Rules

Expand Down
Loading
Loading