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
Next Next commit
TUnit.AspNetCore Analyzers
  • Loading branch information
thomhurst committed Dec 21, 2025
commit 4ae1558a423576c3426db31e2cc6a80553f40c24
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
<PackageVersion Include="Verify" Version="31.9.0" />
<PackageVersion Include="Verify.NUnit" Version="31.9.0" />
<PackageVersion Include="TUnit" Version="1.6.5" />
<PackageVersion Include="TUnit.AspNetCore" Version="1.6.5" />
<PackageVersion Include="TUnit.Core" Version="1.6.5" />
<PackageVersion Include="TUnit.Assertions" Version="1.6.5" />
<PackageVersion Include="Verify.TUnit" Version="31.9.0" />
Expand Down
1 change: 1 addition & 0 deletions TUnit.Analyzers.Tests/TUnit.Analyzers.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TUnit.Analyzers.CodeFixers\TUnit.Analyzers.CodeFixers.csproj" />
<ProjectReference Include="..\TUnit.AspNetCore.Analyzers\TUnit.AspNetCore.Analyzers.csproj" />
<ProjectReference Include="..\TUnit.Engine\TUnit.Engine.csproj" />
<ProjectReference Include="..\TUnit.Analyzers\TUnit.Analyzers.csproj" />
<ProjectReference Include="..\TUnit.TestProject.Library\TUnit.TestProject.Library.csproj" />
Expand Down
308 changes: 308 additions & 0 deletions TUnit.Analyzers.Tests/WebApplicationFactoryAccessAnalyzerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
using AspNetCoreRules = TUnit.AspNetCore.Analyzers.Rules;
using Verifier = TUnit.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier<TUnit.AspNetCore.Analyzers.WebApplicationFactoryAccessAnalyzer>;

namespace TUnit.Analyzers.Tests;

public class WebApplicationFactoryAccessAnalyzerTests
{
private const string WebApplicationTestStub = """
namespace TUnit.AspNetCore
{
public abstract class WebApplicationTest
{
public int UniqueId { get; }
}

public abstract class WebApplicationTest<TFactory, TEntryPoint> : WebApplicationTest
where TFactory : class, new()
where TEntryPoint : class
{
public TFactory GlobalFactory { get; set; } = null!;
public object Factory { get; } = null!;
public System.IServiceProvider Services { get; } = null!;
public object? HttpCapture { get; }

protected virtual System.Threading.Tasks.Task SetupAsync() => System.Threading.Tasks.Task.CompletedTask;
}
}
""";

[Test]
public async Task No_Error_When_Accessing_Factory_In_Test_Method()
{
await Verifier
.VerifyAnalyzerAsync(
$$"""
using TUnit.Core;
{{WebApplicationTestStub}}

public class MyFactory { }
public class Program { }

public class MyTests : TUnit.AspNetCore.WebApplicationTest<MyFactory, Program>
{
[Test]
public void MyTest()
{
var factory = Factory;
var services = Services;
}
}
"""
);
}

[Test]
public async Task Error_When_Accessing_Factory_In_Constructor()
{
await Verifier
.VerifyAnalyzerAsync(
$$"""
using TUnit.Core;
{{WebApplicationTestStub}}

public class MyFactory { }
public class Program { }

public class MyTests : TUnit.AspNetCore.WebApplicationTest<MyFactory, Program>
{
public MyTests()
{
var factory = {|#0:Factory|};
}

[Test]
public void MyTest()
{
}
}
""",
Verifier.Diagnostic(AspNetCoreRules.FactoryAccessedTooEarly)
.WithLocation(0)
.WithArguments("Factory", "constructor")
);
}

[Test]
public async Task Error_When_Accessing_Services_In_Constructor()
{
await Verifier
.VerifyAnalyzerAsync(
$$"""
using TUnit.Core;
{{WebApplicationTestStub}}

public class MyFactory { }
public class Program { }

public class MyTests : TUnit.AspNetCore.WebApplicationTest<MyFactory, Program>
{
public MyTests()
{
var services = {|#0:Services|};
}

[Test]
public void MyTest()
{
}
}
""",
Verifier.Diagnostic(AspNetCoreRules.FactoryAccessedTooEarly)
.WithLocation(0)
.WithArguments("Services", "constructor")
);
}

[Test]
public async Task Error_When_Accessing_Factory_In_SetupAsync()
{
await Verifier
.VerifyAnalyzerAsync(
$$"""
using TUnit.Core;
using System.Threading.Tasks;
{{WebApplicationTestStub}}

public class MyFactory { }
public class Program { }

public class MyTests : TUnit.AspNetCore.WebApplicationTest<MyFactory, Program>
{
protected override Task SetupAsync()
{
var factory = {|#0:Factory|};
return Task.CompletedTask;
}

[Test]
public void MyTest()
{
}
}
""",
Verifier.Diagnostic(AspNetCoreRules.FactoryAccessedTooEarly)
.WithLocation(0)
.WithArguments("Factory", "SetupAsync")
);
}

[Test]
public async Task Error_When_Accessing_HttpCapture_In_SetupAsync()
{
await Verifier
.VerifyAnalyzerAsync(
$$"""
using TUnit.Core;
using System.Threading.Tasks;
{{WebApplicationTestStub}}

public class MyFactory { }
public class Program { }

public class MyTests : TUnit.AspNetCore.WebApplicationTest<MyFactory, Program>
{
protected override Task SetupAsync()
{
var capture = {|#0:HttpCapture|};
return Task.CompletedTask;
}

[Test]
public void MyTest()
{
}
}
""",
Verifier.Diagnostic(AspNetCoreRules.FactoryAccessedTooEarly)
.WithLocation(0)
.WithArguments("HttpCapture", "SetupAsync")
);
}

[Test]
public async Task Error_When_Accessing_GlobalFactory_In_Constructor()
{
// GlobalFactory is NOT available in constructor - it's injected via property injection after construction
await Verifier
.VerifyAnalyzerAsync(
$$"""
using TUnit.Core;
{{WebApplicationTestStub}}

public class MyFactory { }
public class Program { }

public class MyTests : TUnit.AspNetCore.WebApplicationTest<MyFactory, Program>
{
public MyTests()
{
var factory = {|#0:GlobalFactory|};
}

[Test]
public void MyTest()
{
}
}
""",
Verifier.Diagnostic(AspNetCoreRules.FactoryAccessedTooEarly)
.WithLocation(0)
.WithArguments("GlobalFactory", "constructor")
);
}

[Test]
public async Task No_Error_When_Accessing_GlobalFactory_In_SetupAsync()
{
// GlobalFactory IS available in SetupAsync - it's injected before SetupAsync runs
await Verifier
.VerifyAnalyzerAsync(
$$"""
using TUnit.Core;
using System.Threading.Tasks;
{{WebApplicationTestStub}}

public class MyFactory { }
public class Program { }

public class MyTests : TUnit.AspNetCore.WebApplicationTest<MyFactory, Program>
{
protected override Task SetupAsync()
{
var factory = GlobalFactory;
return Task.CompletedTask;
}

[Test]
public void MyTest()
{
}
}
"""
);
}

[Test]
public async Task No_Error_When_Accessing_UniqueId_In_SetupAsync()
{
// UniqueId IS available in SetupAsync
await Verifier
.VerifyAnalyzerAsync(
$$"""
using TUnit.Core;
using System.Threading.Tasks;
{{WebApplicationTestStub}}

public class MyFactory { }
public class Program { }

public class MyTests : TUnit.AspNetCore.WebApplicationTest<MyFactory, Program>
{
protected override Task SetupAsync()
{
var id = UniqueId;
return Task.CompletedTask;
}

[Test]
public void MyTest()
{
}
}
"""
);
}

[Test]
public async Task No_Error_For_Unrelated_Factory_Property()
{
// A property named Factory on an unrelated class should not trigger the analyzer
await Verifier
.VerifyAnalyzerAsync(
"""
using TUnit.Core;

public class SomeClass
{
public object Factory { get; } = null!;
}

public class MyTests
{
private SomeClass _someClass = new();

public MyTests()
{
var factory = _someClass.Factory;
}

[Test]
public void MyTest()
{
}
}
"""
);
}
}
6 changes: 6 additions & 0 deletions TUnit.AspNetCore.Analyzers/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## Release 1.0

### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
10 changes: 10 additions & 0 deletions TUnit.AspNetCore.Analyzers/AnalyzerReleases.Unshipped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
### New Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
TUnit0062 | Usage | Error | Factory property accessed before initialization in WebApplicationTest

### Removed Rules

Rule ID | Category | Severity | Notes
--------|----------|----------|-------
16 changes: 16 additions & 0 deletions TUnit.AspNetCore.Analyzers/ConcurrentDiagnosticAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.CodeAnalysis.Diagnostics;

namespace TUnit.AspNetCore.Analyzers;

public abstract class ConcurrentDiagnosticAnalyzer : DiagnosticAnalyzer
{
public sealed override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();

InitializeInternal(context);
}

protected abstract void InitializeInternal(AnalysisContext context);
}
Loading