Skip to content
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
@@ -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,128 @@
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 (ContainsUsing(compilationUnit.Usings, namespaceName) ||
compilationUnit.Members.Any(m => ContainsUsingInNamespace(m, namespaceName)))
{
return compilationUnit;
}

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

return compilationUnit.AddUsings(newUsing);
}

private static bool ContainsUsingInNamespace(MemberDeclarationSyntax member, string namespaceName) => member switch
{
BaseNamespaceDeclarationSyntax ns =>
ContainsUsing(ns.Usings, namespaceName) ||
ns.Members.Any(m => ContainsUsingInNamespace(m, namespaceName)),
_ => false,
};

private static bool ContainsUsing(SyntaxList<UsingDirectiveSyntax> usings, string namespaceName)
{
foreach (var directive in usings)
{
if (directive.Alias is null &&
directive.StaticKeyword.IsKind(SyntaxKind.None) &&
directive.Name?.ToString() == namespaceName)
{
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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_Fires_Once_For_Partial_Class()
{
await Verifier.VerifyAnalyzerAsync(
$$"""
{{WebApplicationFactoryStubs.Source}}

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

public partial class MyFactory
{
}
""",
Verifier.Diagnostic(Rules.DirectWebApplicationFactoryInheritance)
.WithLocation(0)
.WithArguments("MyFactory"));
}

[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
Loading
Loading