From 4da0e0a1c31762c112013027222f55298ae3d784 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Wed, 3 Jun 2026 23:47:25 +0100
Subject: [PATCH 1/3] fix(analyzers): decouple code fixers from Rules to
prevent MissingFieldException in VS (#6157)
Code fixer assemblies ship in the version-agnostic analyzers/dotnet/cs
folder and resolve their analyzer-assembly dependency at runtime by
simple name. Visual Studio cannot unload analyzer assemblies, so after
a package update (or with mixed TUnit versions in one session) the new
code fixers can bind against a stale TUnit.Analyzers.dll. The eagerly
evaluated FixableDiagnosticIds then JITs a field reference to
Rules.MSTestMigration / Rules.NUnitMigration, which the stale assembly
does not contain, throwing MissingFieldException on every lightbulb
pass.
Fix: introduce DiagnosticIds const classes (compile-time baked into
consuming IL) and remove every Rules reference from both code fixer
assemblies. Add IL-level regression tests asserting the built code
fixer dlls contain no TypeReference to Rules.
---
.../Base/BaseMigrationCodeFixProvider.cs | 8 ++
.../InheritsTestsCodeFixProvider.cs | 8 +-
.../MSTestMigrationCodeFixProvider.cs | 2 +-
.../MatrixDataSourceCodeFixProvider.cs | 2 +-
.../NUnitMigrationCodeFixProvider.cs | 2 +-
...TimeoutCancellationTokenCodeFixProvider.cs | 2 +-
.../VirtualHookOverrideCodeFixProvider.cs | 2 +-
.../XUnitMigrationCodeFixProvider.cs | 4 +-
.../CodeFixerRulesDecouplingTests.cs | 47 ++++++++
TUnit.Analyzers/DiagnosticIds.cs | 71 +++++++++++
TUnit.Analyzers/Rules.cs | 112 +++++++++---------
.../CodeFixerRulesDecouplingTests.cs | 46 +++++++
.../AwaitAssertionCodeFixProvider.cs | 2 +-
.../CollectionIsEqualToCodeFixProvider.cs | 2 +-
.../XUnitAssertionCodeFixProvider.cs | 2 +-
TUnit.Assertions.Analyzers/DiagnosticIds.cs | 32 +++++
TUnit.Assertions.Analyzers/Rules.cs | 34 +++---
17 files changed, 292 insertions(+), 86 deletions(-)
create mode 100644 TUnit.Analyzers.Tests/CodeFixerRulesDecouplingTests.cs
create mode 100644 TUnit.Analyzers/DiagnosticIds.cs
create mode 100644 TUnit.Assertions.Analyzers.CodeFixers.Tests/CodeFixerRulesDecouplingTests.cs
create mode 100644 TUnit.Assertions.Analyzers/DiagnosticIds.cs
diff --git a/TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs
index eedfc5a6b3..7dc7bc8b9c 100644
--- a/TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs
+++ b/TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs
@@ -13,7 +13,15 @@ namespace TUnit.Analyzers.CodeFixers.Base;
public abstract class BaseMigrationCodeFixProvider : CodeFixProvider
{
protected abstract string FrameworkName { get; }
+
+ ///
+ /// The fixable diagnostic ID. Implementations MUST return a constant
+ /// (never Rules.X.Id): VS evaluates eagerly, and a runtime
+ /// field reference into TUnit.Analyzers can bind against a stale copy already loaded in the IDE,
+ /// throwing MissingFieldException. See https://github.com/thomhurst/TUnit/issues/6157.
+ ///
protected abstract string DiagnosticId { get; }
+
protected abstract string CodeFixTitle { get; }
public sealed override ImmutableArray FixableDiagnosticIds =>
diff --git a/TUnit.Analyzers.CodeFixers/InheritsTestsCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/InheritsTestsCodeFixProvider.cs
index 1437836a79..4c20753375 100644
--- a/TUnit.Analyzers.CodeFixers/InheritsTestsCodeFixProvider.cs
+++ b/TUnit.Analyzers.CodeFixers/InheritsTestsCodeFixProvider.cs
@@ -16,8 +16,10 @@ namespace TUnit.Analyzers.CodeFixers;
[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(InheritsTestsCodeFixProvider)), Shared]
public class InheritsTestsCodeFixProvider : CodeFixProvider
{
+ private const string CodeFixTitle = "Add [InheritsTests] attribute";
+
public sealed override ImmutableArray FixableDiagnosticIds { get; } =
- ImmutableArray.Create(Rules.DoesNotInheritTestsWarning.Id);
+ ImmutableArray.Create(DiagnosticIds.DoesNotInheritTestsWarning);
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
@@ -38,9 +40,9 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
context.RegisterCodeFix(
CodeAction.Create(
- title: Rules.DoesNotInheritTestsWarning.Title.ToString(),
+ title: CodeFixTitle,
createChangedDocument: c => AddInheritsTests(context.Document, classDeclarationSyntax, c),
- equivalenceKey: Rules.DoesNotInheritTestsWarning.Title.ToString()),
+ equivalenceKey: CodeFixTitle),
diagnostic);
}
}
diff --git a/TUnit.Analyzers.CodeFixers/MSTestMigrationCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/MSTestMigrationCodeFixProvider.cs
index 5dcb09e5f6..f05c9809f8 100644
--- a/TUnit.Analyzers.CodeFixers/MSTestMigrationCodeFixProvider.cs
+++ b/TUnit.Analyzers.CodeFixers/MSTestMigrationCodeFixProvider.cs
@@ -14,7 +14,7 @@ namespace TUnit.Analyzers.CodeFixers;
public class MSTestMigrationCodeFixProvider : BaseMigrationCodeFixProvider
{
protected override string FrameworkName => "MSTest";
- protected override string DiagnosticId => Rules.MSTestMigration.Id;
+ protected override string DiagnosticId => DiagnosticIds.MSTestMigration;
protected override string CodeFixTitle => "Convert MSTest code to TUnit";
protected override bool ShouldAddTUnitUsings() => true;
diff --git a/TUnit.Analyzers.CodeFixers/MatrixDataSourceCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/MatrixDataSourceCodeFixProvider.cs
index e0caf920c4..eaed2b9d05 100644
--- a/TUnit.Analyzers.CodeFixers/MatrixDataSourceCodeFixProvider.cs
+++ b/TUnit.Analyzers.CodeFixers/MatrixDataSourceCodeFixProvider.cs
@@ -15,7 +15,7 @@ public class MatrixDataSourceCodeFixProvider : CodeFixProvider
private const string Title = "Add [MatrixDataSource]";
public sealed override ImmutableArray FixableDiagnosticIds { get; } =
- ImmutableArray.Create(Rules.MatrixDataSourceAttributeRequired.Id);
+ ImmutableArray.Create(DiagnosticIds.MatrixDataSourceAttributeRequired);
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
diff --git a/TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs
index 23519686cf..601042a1a1 100644
--- a/TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs
+++ b/TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs
@@ -14,7 +14,7 @@ namespace TUnit.Analyzers.CodeFixers;
public class NUnitMigrationCodeFixProvider : BaseMigrationCodeFixProvider
{
protected override string FrameworkName => "NUnit";
- protected override string DiagnosticId => Rules.NUnitMigration.Id;
+ protected override string DiagnosticId => DiagnosticIds.NUnitMigration;
protected override string CodeFixTitle => "Convert NUnit code to TUnit";
protected override AttributeRewriter CreateAttributeRewriter(Compilation compilation)
diff --git a/TUnit.Analyzers.CodeFixers/TimeoutCancellationTokenCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/TimeoutCancellationTokenCodeFixProvider.cs
index 69da900156..de5c34d82a 100644
--- a/TUnit.Analyzers.CodeFixers/TimeoutCancellationTokenCodeFixProvider.cs
+++ b/TUnit.Analyzers.CodeFixers/TimeoutCancellationTokenCodeFixProvider.cs
@@ -17,7 +17,7 @@ public class TimeoutCancellationTokenCodeFixProvider : CodeFixProvider
private const string ParameterName = "cancellationToken";
public sealed override ImmutableArray FixableDiagnosticIds { get; } =
- ImmutableArray.Create(Rules.MissingTimeoutCancellationTokenAttributes.Id);
+ ImmutableArray.Create(DiagnosticIds.MissingTimeoutCancellationTokenAttributes);
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
diff --git a/TUnit.Analyzers.CodeFixers/VirtualHookOverrideCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/VirtualHookOverrideCodeFixProvider.cs
index 4c58dc9e84..eba7c7eab6 100644
--- a/TUnit.Analyzers.CodeFixers/VirtualHookOverrideCodeFixProvider.cs
+++ b/TUnit.Analyzers.CodeFixers/VirtualHookOverrideCodeFixProvider.cs
@@ -15,7 +15,7 @@ public class VirtualHookOverrideCodeFixProvider : CodeFixProvider
private const string Title = "Remove redundant hook attribute";
public sealed override ImmutableArray FixableDiagnosticIds { get; } =
- ImmutableArray.Create(Rules.RedundantHookAttributeOnOverride.Id);
+ ImmutableArray.Create(DiagnosticIds.RedundantHookAttributeOnOverride);
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
diff --git a/TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs
index 87e55ed2ec..75a2178a63 100644
--- a/TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs
+++ b/TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs
@@ -13,8 +13,8 @@ namespace TUnit.Analyzers.CodeFixers;
public class XUnitMigrationCodeFixProvider : BaseMigrationCodeFixProvider
{
protected override string FrameworkName => "XUnit";
- protected override string DiagnosticId => Rules.XunitMigration.Id;
- protected override string CodeFixTitle => Rules.XunitMigration.Title.ToString();
+ protected override string DiagnosticId => DiagnosticIds.XunitMigration;
+ protected override string CodeFixTitle => "Convert xUnit code to TUnit";
protected override bool ShouldAddTUnitUsings() => true;
diff --git a/TUnit.Analyzers.Tests/CodeFixerRulesDecouplingTests.cs b/TUnit.Analyzers.Tests/CodeFixerRulesDecouplingTests.cs
new file mode 100644
index 0000000000..fb9f6d74ac
--- /dev/null
+++ b/TUnit.Analyzers.Tests/CodeFixerRulesDecouplingTests.cs
@@ -0,0 +1,47 @@
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+using TUnit.Analyzers.CodeFixers;
+
+namespace TUnit.Analyzers.Tests;
+
+///
+/// Guards against https://github.com/thomhurst/TUnit/issues/6157.
+///
+/// TUnit.Analyzers.CodeFixers.dll ships in the version-agnostic analyzers/dotnet/cs folder and
+/// resolves its TUnit.Analyzers dependency at runtime by simple name. Visual Studio cannot unload
+/// analyzer assemblies, so after a package update (or with mixed TUnit versions in one VS session)
+/// the new code fixers can bind against a stale TUnit.Analyzers.dll. Any IL reference to the
+/// Rules type (e.g. Rules.MSTestMigration.Id inside the eagerly-evaluated
+/// FixableDiagnosticIds) then throws MissingFieldException for rules the stale assembly
+/// doesn't have. Code fixers must use the compile-time-baked DiagnosticIds constants instead.
+///
+public class CodeFixerRulesDecouplingTests
+{
+ [Test]
+ public async Task CodeFixers_Assembly_Has_No_Reference_To_Rules_Type()
+ {
+ var assemblyPath = typeof(MSTestMigrationCodeFixProvider).Assembly.Location;
+
+ using var stream = File.OpenRead(assemblyPath);
+ using var peReader = new PEReader(stream);
+ var metadata = peReader.GetMetadataReader();
+
+ var rulesReferences = new List();
+
+ foreach (var handle in metadata.TypeReferences)
+ {
+ var typeReference = metadata.GetTypeReference(handle);
+
+ if (metadata.GetString(typeReference.Name) == "Rules" &&
+ metadata.GetString(typeReference.Namespace) == "TUnit.Analyzers")
+ {
+ rulesReferences.Add($"{metadata.GetString(typeReference.Namespace)}.{metadata.GetString(typeReference.Name)}");
+ }
+ }
+
+ await TUnit.Assertions.Assert.That(rulesReferences)
+ .IsEmpty()
+ .Because("TUnit.Analyzers.CodeFixers must not reference TUnit.Analyzers.Rules at runtime - " +
+ "use DiagnosticIds constants instead (see issue #6157)");
+ }
+}
diff --git a/TUnit.Analyzers/DiagnosticIds.cs b/TUnit.Analyzers/DiagnosticIds.cs
new file mode 100644
index 0000000000..4612aa6f02
--- /dev/null
+++ b/TUnit.Analyzers/DiagnosticIds.cs
@@ -0,0 +1,71 @@
+namespace TUnit.Analyzers;
+
+///
+/// Diagnostic ID constants for all TUnit analyzer rules.
+///
+///
+/// Code fix providers (TUnit.Analyzers.CodeFixers) MUST reference these constants instead of
+/// Rules.X.Id. Constants are baked into the consuming assembly at compile time, so the
+/// code fixers carry no runtime reference to the type. A runtime reference
+/// can bind against a stale TUnit.Analyzers.dll already loaded in Visual Studio (analyzers
+/// cannot be unloaded), throwing for newly added
+/// rules. See https://github.com/thomhurst/TUnit/issues/6157.
+///
+public static class DiagnosticIds
+{
+ public const string WrongArgumentTypeTestData = "TUnit0001";
+ public const string NoTestDataProvided = "TUnit0002";
+ public const string NoMethodFound = "TUnit0004";
+ public const string MethodParameterBadNullability = "TUnit0005";
+ public const string MethodMustBeStatic = "TUnit0007";
+ public const string MethodMustBePublic = "TUnit0008";
+ public const string MethodMustNotBeAbstract = "TUnit0009";
+ public const string MethodMustBeParameterless = "TUnit0010";
+ public const string MethodMustReturnData = "TUnit0011";
+ public const string TooManyArgumentsInTestMethod = "TUnit0013";
+ public const string PublicMethodMissingTestAttribute = "TUnit0014";
+ public const string MissingTimeoutCancellationTokenAttributes = "TUnit0015";
+ public const string MethodMustNotBeStatic = "TUnit0016";
+ public const string ConflictingExplicitAttributes = "TUnit0017";
+ public const string InstanceAssignmentInTestClass = "TUnit0018";
+ public const string MissingTestAttribute = "TUnit0019";
+ public const string Dispose_Member_In_Cleanup = "TUnit0023";
+ public const string UnknownParameters = "TUnit0027";
+ public const string DoNotOverrideAttributeUsageMetadata = "TUnit0028";
+ public const string DuplicateSingleAttribute = "TUnit0029";
+ public const string DoesNotInheritTestsWarning = "TUnit0030";
+ public const string AsyncVoidMethod = "TUnit0031";
+ public const string DependsOnConflicts = "TUnit0033";
+ public const string NoMainMethod = "TUnit0034";
+ public const string NoDataSourceProvided = "TUnit0038";
+ public const string SingleTestContextParameterRequired = "TUnit0039";
+ public const string SingleClassHookContextParameterRequired = "TUnit0040";
+ public const string SingleAssemblyHookContextParameterRequired = "TUnit0041";
+ public const string GlobalHooksSeparateClass = "TUnit0042";
+ public const string PropertyRequiredNotSet = "TUnit0043";
+ public const string MustHavePropertySetter = "TUnit0044";
+ public const string TooManyDataAttributes = "TUnit0045";
+ public const string ReturnFunc = "TUnit0046";
+ public const string AsyncLocalCallFlowValues = "TUnit0047";
+ public const string InstanceTestMethod = "TUnit0048";
+ public const string MatrixDataSourceAttributeRequired = "TUnit0049";
+ public const string TooManyArguments = "TUnit0050";
+ public const string TypeMustBePublic = "TUnit0051";
+ public const string MultipleConstructorsWithoutTestConstructor = "TUnit0052";
+ public const string XunitMigration = "TUXU0001";
+ public const string NUnitMigration = "TUNU0001";
+ public const string MSTestMigration = "TUMS0001";
+ public const string OverwriteConsole = "TUnit0055";
+ public const string InstanceMethodSource = "TUnit0056";
+ public const string HookContextParameterOptional = "TUnit0057";
+ public const string HookUnknownParameters = "TUnit0058";
+ public const string AbstractTestClassWithDataSources = "TUnit0059";
+ public const string PotentialEmptyDataSource = "TUnit0060";
+ public const string NoAccessibleConstructor = "TUnit0061";
+ public const string CancellationTokenMustBeLastParameter = "TUnit0062";
+ public const string CombinedDataSourceAttributeRequired = "TUnit0070";
+ public const string CombinedDataSourceMissingParameterDataSource = "TUnit0071";
+ public const string CombinedDataSourceConflictWithMatrix = "TUnit0072";
+ public const string MissingPolyfillPackage = "TUnit0073";
+ public const string RedundantHookAttributeOnOverride = "TUnit0074";
+}
diff --git a/TUnit.Analyzers/Rules.cs b/TUnit.Analyzers/Rules.cs
index 0b6cba34a9..e6f7d8fffc 100644
--- a/TUnit.Analyzers/Rules.cs
+++ b/TUnit.Analyzers/Rules.cs
@@ -1,4 +1,4 @@
-using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis;
namespace TUnit.Analyzers;
@@ -7,171 +7,171 @@ public static class Rules
private const string UsageCategory = "Usage";
public static readonly DiagnosticDescriptor WrongArgumentTypeTestData =
- CreateDescriptor("TUnit0001", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.WrongArgumentTypeTestData, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor NoTestDataProvided =
- CreateDescriptor("TUnit0002", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.NoTestDataProvided, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor NoMethodFound =
- CreateDescriptor("TUnit0004", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.NoMethodFound, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor MethodParameterBadNullability =
- CreateDescriptor("TUnit0005", UsageCategory, DiagnosticSeverity.Warning);
+ CreateDescriptor(DiagnosticIds.MethodParameterBadNullability, UsageCategory, DiagnosticSeverity.Warning);
public static readonly DiagnosticDescriptor MethodMustBeStatic =
- CreateDescriptor("TUnit0007", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.MethodMustBeStatic, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor MethodMustBePublic =
- CreateDescriptor("TUnit0008", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.MethodMustBePublic, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor MethodMustNotBeAbstract =
- CreateDescriptor("TUnit0009", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.MethodMustNotBeAbstract, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor MethodMustBeParameterless =
- CreateDescriptor("TUnit0010", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.MethodMustBeParameterless, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor MethodMustReturnData =
- CreateDescriptor("TUnit0011", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.MethodMustReturnData, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor TooManyArgumentsInTestMethod =
- CreateDescriptor("TUnit0013", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.TooManyArgumentsInTestMethod, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor PublicMethodMissingTestAttribute =
- CreateDescriptor("TUnit0014", UsageCategory, DiagnosticSeverity.Warning);
+ CreateDescriptor(DiagnosticIds.PublicMethodMissingTestAttribute, UsageCategory, DiagnosticSeverity.Warning);
public static readonly DiagnosticDescriptor MissingTimeoutCancellationTokenAttributes =
- CreateDescriptor("TUnit0015", UsageCategory, DiagnosticSeverity.Warning);
+ CreateDescriptor(DiagnosticIds.MissingTimeoutCancellationTokenAttributes, UsageCategory, DiagnosticSeverity.Warning);
public static readonly DiagnosticDescriptor CancellationTokenMustBeLastParameter =
- CreateDescriptor("TUnit0062", UsageCategory, DiagnosticSeverity.Warning);
+ CreateDescriptor(DiagnosticIds.CancellationTokenMustBeLastParameter, UsageCategory, DiagnosticSeverity.Warning);
public static readonly DiagnosticDescriptor MethodMustNotBeStatic =
- CreateDescriptor("TUnit0016", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.MethodMustNotBeStatic, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor ConflictingExplicitAttributes =
- CreateDescriptor("TUnit0017", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.ConflictingExplicitAttributes, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor InstanceAssignmentInTestClass =
- CreateDescriptor("TUnit0018", UsageCategory, DiagnosticSeverity.Warning);
+ CreateDescriptor(DiagnosticIds.InstanceAssignmentInTestClass, UsageCategory, DiagnosticSeverity.Warning);
public static readonly DiagnosticDescriptor MissingTestAttribute =
- CreateDescriptor("TUnit0019", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.MissingTestAttribute, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor Dispose_Member_In_Cleanup =
- CreateDescriptor("TUnit0023", UsageCategory, DiagnosticSeverity.Warning);
+ CreateDescriptor(DiagnosticIds.Dispose_Member_In_Cleanup, UsageCategory, DiagnosticSeverity.Warning);
public static readonly DiagnosticDescriptor UnknownParameters =
- CreateDescriptor("TUnit0027", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.UnknownParameters, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor DoNotOverrideAttributeUsageMetadata =
- CreateDescriptor("TUnit0028", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.DoNotOverrideAttributeUsageMetadata, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor DuplicateSingleAttribute =
- CreateDescriptor("TUnit0029", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.DuplicateSingleAttribute, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor DoesNotInheritTestsWarning =
- CreateDescriptor("TUnit0030", UsageCategory, DiagnosticSeverity.Warning);
+ CreateDescriptor(DiagnosticIds.DoesNotInheritTestsWarning, UsageCategory, DiagnosticSeverity.Warning);
public static readonly DiagnosticDescriptor AsyncVoidMethod =
- CreateDescriptor("TUnit0031", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.AsyncVoidMethod, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor DependsOnConflicts =
- CreateDescriptor("TUnit0033", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.DependsOnConflicts, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor NoMainMethod =
- CreateDescriptor("TUnit0034", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.NoMainMethod, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor NoDataSourceProvided =
- CreateDescriptor("TUnit0038", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.NoDataSourceProvided, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor SingleTestContextParameterRequired =
- CreateDescriptor("TUnit0039", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.SingleTestContextParameterRequired, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor SingleClassHookContextParameterRequired =
- CreateDescriptor("TUnit0040", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.SingleClassHookContextParameterRequired, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor SingleAssemblyHookContextParameterRequired =
- CreateDescriptor("TUnit0041", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.SingleAssemblyHookContextParameterRequired, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor GlobalHooksSeparateClass =
- CreateDescriptor("TUnit0042", UsageCategory, DiagnosticSeverity.Warning);
+ CreateDescriptor(DiagnosticIds.GlobalHooksSeparateClass, UsageCategory, DiagnosticSeverity.Warning);
public static readonly DiagnosticDescriptor PropertyRequiredNotSet =
- CreateDescriptor("TUnit0043", UsageCategory, DiagnosticSeverity.Info);
+ CreateDescriptor(DiagnosticIds.PropertyRequiredNotSet, UsageCategory, DiagnosticSeverity.Info);
public static readonly DiagnosticDescriptor MustHavePropertySetter =
- CreateDescriptor("TUnit0044", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.MustHavePropertySetter, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor TooManyDataAttributes =
- CreateDescriptor("TUnit0045", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.TooManyDataAttributes, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor ReturnFunc =
- CreateDescriptor("TUnit0046", UsageCategory, DiagnosticSeverity.Warning);
+ CreateDescriptor(DiagnosticIds.ReturnFunc, UsageCategory, DiagnosticSeverity.Warning);
public static readonly DiagnosticDescriptor AsyncLocalCallFlowValues =
- CreateDescriptor("TUnit0047", UsageCategory, DiagnosticSeverity.Warning);
+ CreateDescriptor(DiagnosticIds.AsyncLocalCallFlowValues, UsageCategory, DiagnosticSeverity.Warning);
public static DiagnosticDescriptor InstanceTestMethod =
- CreateDescriptor("TUnit0048", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.InstanceTestMethod, UsageCategory, DiagnosticSeverity.Error);
public static DiagnosticDescriptor MatrixDataSourceAttributeRequired =
- CreateDescriptor("TUnit0049", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.MatrixDataSourceAttributeRequired, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor TooManyArguments =
- CreateDescriptor("TUnit0050", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.TooManyArguments, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor TypeMustBePublic =
- CreateDescriptor("TUnit0051", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.TypeMustBePublic, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor MultipleConstructorsWithoutTestConstructor =
- CreateDescriptor("TUnit0052", UsageCategory, DiagnosticSeverity.Warning);
+ CreateDescriptor(DiagnosticIds.MultipleConstructorsWithoutTestConstructor, UsageCategory, DiagnosticSeverity.Warning);
public static readonly DiagnosticDescriptor XunitMigration =
- CreateDescriptor("TUXU0001", UsageCategory, DiagnosticSeverity.Info);
+ CreateDescriptor(DiagnosticIds.XunitMigration, UsageCategory, DiagnosticSeverity.Info);
public static readonly DiagnosticDescriptor NUnitMigration =
- CreateDescriptor("TUNU0001", UsageCategory, DiagnosticSeverity.Info);
+ CreateDescriptor(DiagnosticIds.NUnitMigration, UsageCategory, DiagnosticSeverity.Info);
public static readonly DiagnosticDescriptor MSTestMigration =
- CreateDescriptor("TUMS0001", UsageCategory, DiagnosticSeverity.Info);
+ CreateDescriptor(DiagnosticIds.MSTestMigration, UsageCategory, DiagnosticSeverity.Info);
public static readonly DiagnosticDescriptor OverwriteConsole =
- CreateDescriptor("TUnit0055", UsageCategory, DiagnosticSeverity.Warning);
+ CreateDescriptor(DiagnosticIds.OverwriteConsole, UsageCategory, DiagnosticSeverity.Warning);
public static readonly DiagnosticDescriptor CombinedDataSourceAttributeRequired =
- CreateDescriptor("TUnit0070", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.CombinedDataSourceAttributeRequired, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor CombinedDataSourceMissingParameterDataSource =
- CreateDescriptor("TUnit0071", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.CombinedDataSourceMissingParameterDataSource, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor CombinedDataSourceConflictWithMatrix =
- CreateDescriptor("TUnit0072", UsageCategory, DiagnosticSeverity.Warning);
+ CreateDescriptor(DiagnosticIds.CombinedDataSourceConflictWithMatrix, UsageCategory, DiagnosticSeverity.Warning);
public static readonly DiagnosticDescriptor InstanceMethodSource =
- CreateDescriptor("TUnit0056", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.InstanceMethodSource, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor HookContextParameterOptional =
- CreateDescriptor("TUnit0057", UsageCategory, DiagnosticSeverity.Info);
+ CreateDescriptor(DiagnosticIds.HookContextParameterOptional, UsageCategory, DiagnosticSeverity.Info);
public static readonly DiagnosticDescriptor HookUnknownParameters =
- CreateDescriptor("TUnit0058", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.HookUnknownParameters, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor AbstractTestClassWithDataSources =
- CreateDescriptor("TUnit0059", UsageCategory, DiagnosticSeverity.Warning);
+ CreateDescriptor(DiagnosticIds.AbstractTestClassWithDataSources, UsageCategory, DiagnosticSeverity.Warning);
public static readonly DiagnosticDescriptor PotentialEmptyDataSource =
- CreateDescriptor("TUnit0060", UsageCategory, DiagnosticSeverity.Info);
+ CreateDescriptor(DiagnosticIds.PotentialEmptyDataSource, UsageCategory, DiagnosticSeverity.Info);
public static readonly DiagnosticDescriptor NoAccessibleConstructor =
- CreateDescriptor("TUnit0061", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.NoAccessibleConstructor, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor MissingPolyfillPackage =
- CreateDescriptor("TUnit0073", UsageCategory, DiagnosticSeverity.Error,
+ CreateDescriptor(DiagnosticIds.MissingPolyfillPackage, UsageCategory, DiagnosticSeverity.Error,
customTags: [WellKnownDiagnosticTags.CompilationEnd],
helpLinkUri: "https://www.nuget.org/packages/Polyfill");
public static readonly DiagnosticDescriptor RedundantHookAttributeOnOverride =
- CreateDescriptor("TUnit0074", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.RedundantHookAttributeOnOverride, UsageCategory, DiagnosticSeverity.Error);
private static DiagnosticDescriptor CreateDescriptor(string diagnosticId, string category, DiagnosticSeverity severity,
string[]? customTags = null, string? helpLinkUri = null)
diff --git a/TUnit.Assertions.Analyzers.CodeFixers.Tests/CodeFixerRulesDecouplingTests.cs b/TUnit.Assertions.Analyzers.CodeFixers.Tests/CodeFixerRulesDecouplingTests.cs
new file mode 100644
index 0000000000..487817ac41
--- /dev/null
+++ b/TUnit.Assertions.Analyzers.CodeFixers.Tests/CodeFixerRulesDecouplingTests.cs
@@ -0,0 +1,46 @@
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+
+namespace TUnit.Assertions.Analyzers.CodeFixers.Tests;
+
+///
+/// Guards against https://github.com/thomhurst/TUnit/issues/6157.
+///
+/// TUnit.Assertions.Analyzers.CodeFixers.dll ships in the version-agnostic analyzers/dotnet/cs
+/// folder and resolves its TUnit.Assertions.Analyzers dependency at runtime by simple name.
+/// Visual Studio cannot unload analyzer assemblies, so after a package update the new code fixers
+/// can bind against a stale TUnit.Assertions.Analyzers.dll. Any IL reference to the Rules
+/// type inside the eagerly-evaluated FixableDiagnosticIds then throws
+/// MissingFieldException for rules the stale assembly doesn't have. Code fixers must use the
+/// compile-time-baked DiagnosticIds constants instead.
+///
+public class CodeFixerRulesDecouplingTests
+{
+ [Test]
+ public async Task CodeFixers_Assembly_Has_No_Reference_To_Rules_Type()
+ {
+ var assemblyPath = typeof(AwaitAssertionCodeFixProvider).Assembly.Location;
+
+ using var stream = File.OpenRead(assemblyPath);
+ using var peReader = new PEReader(stream);
+ var metadata = peReader.GetMetadataReader();
+
+ var rulesReferences = new List();
+
+ foreach (var handle in metadata.TypeReferences)
+ {
+ var typeReference = metadata.GetTypeReference(handle);
+
+ if (metadata.GetString(typeReference.Name) == "Rules" &&
+ metadata.GetString(typeReference.Namespace) == "TUnit.Assertions.Analyzers")
+ {
+ rulesReferences.Add($"{metadata.GetString(typeReference.Namespace)}.{metadata.GetString(typeReference.Name)}");
+ }
+ }
+
+ await Assert.That(rulesReferences)
+ .IsEmpty()
+ .Because("TUnit.Assertions.Analyzers.CodeFixers must not reference TUnit.Assertions.Analyzers.Rules at runtime - " +
+ "use DiagnosticIds constants instead (see issue #6157)");
+ }
+}
diff --git a/TUnit.Assertions.Analyzers.CodeFixers/AwaitAssertionCodeFixProvider.cs b/TUnit.Assertions.Analyzers.CodeFixers/AwaitAssertionCodeFixProvider.cs
index 0924421d73..911041bf06 100644
--- a/TUnit.Assertions.Analyzers.CodeFixers/AwaitAssertionCodeFixProvider.cs
+++ b/TUnit.Assertions.Analyzers.CodeFixers/AwaitAssertionCodeFixProvider.cs
@@ -14,7 +14,7 @@ namespace TUnit.Assertions.Analyzers.CodeFixers;
public class AwaitAssertionCodeFixProvider : CodeFixProvider
{
public sealed override ImmutableArray FixableDiagnosticIds { get; } =
- ImmutableArray.Create(Rules.AwaitAssertion.Id);
+ ImmutableArray.Create(DiagnosticIds.AwaitAssertion);
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
diff --git a/TUnit.Assertions.Analyzers.CodeFixers/CollectionIsEqualToCodeFixProvider.cs b/TUnit.Assertions.Analyzers.CodeFixers/CollectionIsEqualToCodeFixProvider.cs
index 08f29ca1ba..f64c59ad30 100644
--- a/TUnit.Assertions.Analyzers.CodeFixers/CollectionIsEqualToCodeFixProvider.cs
+++ b/TUnit.Assertions.Analyzers.CodeFixers/CollectionIsEqualToCodeFixProvider.cs
@@ -12,7 +12,7 @@ namespace TUnit.Assertions.Analyzers.CodeFixers;
public class CollectionIsEqualToCodeFixProvider : CodeFixProvider
{
public sealed override ImmutableArray FixableDiagnosticIds { get; } =
- ImmutableArray.Create(Rules.CollectionIsEqualToUsesReferenceEquality.Id);
+ ImmutableArray.Create(DiagnosticIds.CollectionIsEqualToUsesReferenceEquality);
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
diff --git a/TUnit.Assertions.Analyzers.CodeFixers/XUnitAssertionCodeFixProvider.cs b/TUnit.Assertions.Analyzers.CodeFixers/XUnitAssertionCodeFixProvider.cs
index bb8e5ee8d0..0952c090f7 100644
--- a/TUnit.Assertions.Analyzers.CodeFixers/XUnitAssertionCodeFixProvider.cs
+++ b/TUnit.Assertions.Analyzers.CodeFixers/XUnitAssertionCodeFixProvider.cs
@@ -14,7 +14,7 @@ namespace TUnit.Assertions.Analyzers.CodeFixers;
public class XUnitAssertionCodeFixProvider : CodeFixProvider
{
public sealed override ImmutableArray FixableDiagnosticIds { get; } =
- ImmutableArray.Create(Rules.XUnitAssertion.Id);
+ ImmutableArray.Create(DiagnosticIds.XUnitAssertion);
public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
diff --git a/TUnit.Assertions.Analyzers/DiagnosticIds.cs b/TUnit.Assertions.Analyzers/DiagnosticIds.cs
new file mode 100644
index 0000000000..37f3615426
--- /dev/null
+++ b/TUnit.Assertions.Analyzers/DiagnosticIds.cs
@@ -0,0 +1,32 @@
+namespace TUnit.Assertions.Analyzers;
+
+///
+/// Diagnostic ID constants for all TUnit assertions analyzer rules.
+///
+///
+/// Code fix providers (TUnit.Assertions.Analyzers.CodeFixers) MUST reference these constants instead
+/// of Rules.X.Id. Constants are baked into the consuming assembly at compile time, so the
+/// code fixers carry no runtime reference to the type. A runtime reference
+/// can bind against a stale TUnit.Assertions.Analyzers.dll already loaded in Visual Studio
+/// (analyzers cannot be unloaded), throwing for newly
+/// added rules. See https://github.com/thomhurst/TUnit/issues/6157.
+///
+public static class DiagnosticIds
+{
+ public const string MixAndOrConditionsAssertion = "TUnitAssertions0001";
+ public const string AwaitAssertion = "TUnitAssertions0002";
+ public const string CompilerArgumentsPopulated = "TUnitAssertions0003";
+ public const string DisposableUsingMultiple = "TUnitAssertions0004";
+ public const string ConstantValueInAssertThat = "TUnitAssertions0005";
+ public const string ObjectEqualsBaseMethod = "TUnitAssertions0006";
+ public const string DynamicValueInAssertThat = "TUnitAssertions0007";
+ public const string AwaitValueTaskInAssertThat = "TUnitAssertions0008";
+ public const string XUnitAssertion = "TUnitAssertions0009";
+ public const string GenerateAssertionMethodMustBeStatic = "TUnitAssertions0010";
+ public const string GenerateAssertionMethodMustHaveParameter = "TUnitAssertions0011";
+ public const string GenerateAssertionInvalidReturnType = "TUnitAssertions0012";
+ public const string GenerateAssertionShouldBeExtensionMethod = "TUnitAssertions0013";
+ public const string PreferIsNullOverIsEqualToNull = "TUnitAssertions0014";
+ public const string PreferIsTrueOrIsFalseOverIsEqualToBool = "TUnitAssertions0015";
+ public const string CollectionIsEqualToUsesReferenceEquality = "TUnitAssertions0016";
+}
diff --git a/TUnit.Assertions.Analyzers/Rules.cs b/TUnit.Assertions.Analyzers/Rules.cs
index b58ef58988..56cd26480c 100644
--- a/TUnit.Assertions.Analyzers/Rules.cs
+++ b/TUnit.Assertions.Analyzers/Rules.cs
@@ -1,4 +1,4 @@
-using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis;
namespace TUnit.Assertions.Analyzers;
@@ -7,52 +7,52 @@ internal static class Rules
private const string UsageCategory = "Usage";
public static readonly DiagnosticDescriptor MixAndOrConditionsAssertion =
- CreateDescriptor("TUnitAssertions0001", UsageCategory, DiagnosticSeverity.Warning);
+ CreateDescriptor(DiagnosticIds.MixAndOrConditionsAssertion, UsageCategory, DiagnosticSeverity.Warning);
public static readonly DiagnosticDescriptor AwaitAssertion =
- CreateDescriptor("TUnitAssertions0002", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.AwaitAssertion, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor CompilerArgumentsPopulated =
- CreateDescriptor("TUnitAssertions0003", UsageCategory, DiagnosticSeverity.Warning);
+ CreateDescriptor(DiagnosticIds.CompilerArgumentsPopulated, UsageCategory, DiagnosticSeverity.Warning);
public static readonly DiagnosticDescriptor DisposableUsingMultiple =
- CreateDescriptor("TUnitAssertions0004", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.DisposableUsingMultiple, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor ConstantValueInAssertThat =
- CreateDescriptor("TUnitAssertions0005", UsageCategory, DiagnosticSeverity.Warning);
+ CreateDescriptor(DiagnosticIds.ConstantValueInAssertThat, UsageCategory, DiagnosticSeverity.Warning);
public static readonly DiagnosticDescriptor ObjectEqualsBaseMethod =
- CreateDescriptor("TUnitAssertions0006", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.ObjectEqualsBaseMethod, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor DynamicValueInAssertThat =
- CreateDescriptor("TUnitAssertions0007", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.DynamicValueInAssertThat, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor AwaitValueTaskInAssertThat =
- CreateDescriptor("TUnitAssertions0008", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.AwaitValueTaskInAssertThat, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor XUnitAssertion =
- CreateDescriptor("TUnitAssertions0009", UsageCategory, DiagnosticSeverity.Info);
+ CreateDescriptor(DiagnosticIds.XUnitAssertion, UsageCategory, DiagnosticSeverity.Info);
public static readonly DiagnosticDescriptor GenerateAssertionMethodMustBeStatic =
- CreateDescriptor("TUnitAssertions0010", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.GenerateAssertionMethodMustBeStatic, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor GenerateAssertionMethodMustHaveParameter =
- CreateDescriptor("TUnitAssertions0011", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.GenerateAssertionMethodMustHaveParameter, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor GenerateAssertionInvalidReturnType =
- CreateDescriptor("TUnitAssertions0012", UsageCategory, DiagnosticSeverity.Error);
+ CreateDescriptor(DiagnosticIds.GenerateAssertionInvalidReturnType, UsageCategory, DiagnosticSeverity.Error);
public static readonly DiagnosticDescriptor GenerateAssertionShouldBeExtensionMethod =
- CreateDescriptor("TUnitAssertions0013", UsageCategory, DiagnosticSeverity.Warning);
+ CreateDescriptor(DiagnosticIds.GenerateAssertionShouldBeExtensionMethod, UsageCategory, DiagnosticSeverity.Warning);
public static readonly DiagnosticDescriptor PreferIsNullOverIsEqualToNull =
- CreateDescriptor("TUnitAssertions0014", UsageCategory, DiagnosticSeverity.Warning);
+ CreateDescriptor(DiagnosticIds.PreferIsNullOverIsEqualToNull, UsageCategory, DiagnosticSeverity.Warning);
public static readonly DiagnosticDescriptor PreferIsTrueOrIsFalseOverIsEqualToBool =
- CreateDescriptor("TUnitAssertions0015", UsageCategory, DiagnosticSeverity.Warning);
+ CreateDescriptor(DiagnosticIds.PreferIsTrueOrIsFalseOverIsEqualToBool, UsageCategory, DiagnosticSeverity.Warning);
public static readonly DiagnosticDescriptor CollectionIsEqualToUsesReferenceEquality =
- CreateDescriptor("TUnitAssertions0016", UsageCategory, DiagnosticSeverity.Info);
+ CreateDescriptor(DiagnosticIds.CollectionIsEqualToUsesReferenceEquality, UsageCategory, DiagnosticSeverity.Info);
private static DiagnosticDescriptor CreateDescriptor(string diagnosticId, string category, DiagnosticSeverity severity)
{
From 1cf11e40d3daa1694f4b5f508878791be7b1c755 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Thu, 4 Jun 2026 00:15:09 +0100
Subject: [PATCH 2/3] refactor: consolidate IL decoupling guard, cover
AspNetCore codefixer, address review
- Extract duplicated PEReader scan into SharedTestHelpers/RulesDecouplingVerifier
(linked into all three codefixer test projects)
- Cover the third vulnerable codefixer family: TUnit.AspNetCore.Analyzers gets
DiagnosticIds consts + IL guard test (same #6157 pattern, was missed)
- Verifier also flags DiagnosticIds type refs so const -> static readonly drift
fails the test (review feedback)
- Add missing readonly on InstanceTestMethod / MatrixDataSourceAttributeRequired
- Collapse the repeated rationale doc comment to one canonical home
---
SharedTestHelpers/RulesDecouplingVerifier.cs | 61 +++++++++++++++++++
.../Base/BaseMigrationCodeFixProvider.cs | 6 +-
.../CodeFixerRulesDecouplingTests.cs | 34 ++---------
.../TUnit.Analyzers.Tests.csproj | 2 +
TUnit.Analyzers/DiagnosticIds.cs | 13 ++--
TUnit.Analyzers/Rules.cs | 4 +-
...estWebApplicationFactoryCodeFixProvider.cs | 2 +-
.../CodeFixerRulesDecouplingTests.cs | 23 +++++++
.../TUnit.AspNetCore.Analyzers.Tests.csproj | 2 +
TUnit.AspNetCore.Analyzers/DiagnosticIds.cs | 16 +++++
TUnit.AspNetCore.Analyzers/Rules.cs | 6 +-
.../CodeFixerRulesDecouplingTests.cs | 34 ++---------
...sertions.Analyzers.CodeFixers.Tests.csproj | 2 +
TUnit.Assertions.Analyzers/DiagnosticIds.cs | 13 ++--
14 files changed, 134 insertions(+), 84 deletions(-)
create mode 100644 SharedTestHelpers/RulesDecouplingVerifier.cs
create mode 100644 TUnit.AspNetCore.Analyzers.Tests/CodeFixerRulesDecouplingTests.cs
create mode 100644 TUnit.AspNetCore.Analyzers/DiagnosticIds.cs
diff --git a/SharedTestHelpers/RulesDecouplingVerifier.cs b/SharedTestHelpers/RulesDecouplingVerifier.cs
new file mode 100644
index 0000000000..ceb00488b1
--- /dev/null
+++ b/SharedTestHelpers/RulesDecouplingVerifier.cs
@@ -0,0 +1,61 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+
+namespace TUnit.Tests.Shared;
+
+///
+/// Verifies that a code fixer assembly carries no IL reference to its analyzer project's
+/// Rules type. Guards against https://github.com/thomhurst/TUnit/issues/6157.
+///
+///
+/// Code fixer assemblies ship in the version-agnostic analyzers/dotnet/cs folder while the
+/// analyzer assemblies ship per-Roslyn (analyzers/dotnet/roslyn4.x/cs), and the dependency
+/// resolves at runtime by simple name. Visual Studio cannot unload analyzer assemblies, so after a
+/// package update (or with mixed TUnit versions in one VS session) a new code fixer can bind
+/// against a stale analyzer assembly. Any IL reference to the Rules type — e.g.
+/// Rules.X.Id inside the eagerly-evaluated FixableDiagnosticIds — then throws
+/// for rules the stale assembly doesn't have. Code
+/// fixers must use the compile-time-baked DiagnosticIds constants instead, which this
+/// helper enforces at the IL level: a TypeReference to Rules appears for any usage
+/// (field access, typeof, method call), so an empty result proves full decoupling.
+/// DiagnosticIds itself is also scanned — its members must stay const; changing one
+/// to static readonly would silently reintroduce a runtime type reference, which surfaces
+/// here as a TypeReference to DiagnosticIds.
+///
+/// Linked into each code fixer test project via
+/// <Compile Include="..\SharedTestHelpers\RulesDecouplingVerifier.cs">.
+///
+///
+internal static class RulesDecouplingVerifier
+{
+ ///
+ /// Returns the fully-qualified names of all Rules or DiagnosticIds type
+ /// references in whose namespace is
+ /// . An empty list means the assembly is fully decoupled.
+ ///
+ public static List FindRulesTypeReferences(Assembly codeFixersAssembly, string rulesNamespace)
+ {
+ using var stream = File.OpenRead(codeFixersAssembly.Location);
+ using var peReader = new PEReader(stream);
+ var metadata = peReader.GetMetadataReader();
+
+ var rulesReferences = new List();
+
+ foreach (var handle in metadata.TypeReferences)
+ {
+ var typeReference = metadata.GetTypeReference(handle);
+ var name = metadata.GetString(typeReference.Name);
+ var typeNamespace = metadata.GetString(typeReference.Namespace);
+
+ if (name is "Rules" or "DiagnosticIds" && typeNamespace == rulesNamespace)
+ {
+ rulesReferences.Add($"{typeNamespace}.{name}");
+ }
+ }
+
+ return rulesReferences;
+ }
+}
diff --git a/TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs b/TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs
index 7dc7bc8b9c..44d36a0600 100644
--- a/TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs
+++ b/TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs
@@ -15,10 +15,8 @@ public abstract class BaseMigrationCodeFixProvider : CodeFixProvider
protected abstract string FrameworkName { get; }
///
- /// The fixable diagnostic ID. Implementations MUST return a constant
- /// (never Rules.X.Id): VS evaluates eagerly, and a runtime
- /// field reference into TUnit.Analyzers can bind against a stale copy already loaded in the IDE,
- /// throwing MissingFieldException. See https://github.com/thomhurst/TUnit/issues/6157.
+ /// The fixable diagnostic ID. Implementations MUST return a constant,
+ /// never Rules.X.Id — see remarks (issue #6157).
///
protected abstract string DiagnosticId { get; }
diff --git a/TUnit.Analyzers.Tests/CodeFixerRulesDecouplingTests.cs b/TUnit.Analyzers.Tests/CodeFixerRulesDecouplingTests.cs
index fb9f6d74ac..c7cd027112 100644
--- a/TUnit.Analyzers.Tests/CodeFixerRulesDecouplingTests.cs
+++ b/TUnit.Analyzers.Tests/CodeFixerRulesDecouplingTests.cs
@@ -1,43 +1,19 @@
-using System.Reflection.Metadata;
-using System.Reflection.PortableExecutable;
using TUnit.Analyzers.CodeFixers;
+using TUnit.Tests.Shared;
namespace TUnit.Analyzers.Tests;
///
-/// Guards against https://github.com/thomhurst/TUnit/issues/6157.
-///
-/// TUnit.Analyzers.CodeFixers.dll ships in the version-agnostic analyzers/dotnet/cs folder and
-/// resolves its TUnit.Analyzers dependency at runtime by simple name. Visual Studio cannot unload
-/// analyzer assemblies, so after a package update (or with mixed TUnit versions in one VS session)
-/// the new code fixers can bind against a stale TUnit.Analyzers.dll. Any IL reference to the
-/// Rules type (e.g. Rules.MSTestMigration.Id inside the eagerly-evaluated
-/// FixableDiagnosticIds) then throws MissingFieldException for rules the stale assembly
-/// doesn't have. Code fixers must use the compile-time-baked DiagnosticIds constants instead.
+/// Code fixers must use DiagnosticIds constants, never Rules.X — see
+/// for the full rationale (issue #6157).
///
public class CodeFixerRulesDecouplingTests
{
[Test]
public async Task CodeFixers_Assembly_Has_No_Reference_To_Rules_Type()
{
- var assemblyPath = typeof(MSTestMigrationCodeFixProvider).Assembly.Location;
-
- using var stream = File.OpenRead(assemblyPath);
- using var peReader = new PEReader(stream);
- var metadata = peReader.GetMetadataReader();
-
- var rulesReferences = new List();
-
- foreach (var handle in metadata.TypeReferences)
- {
- var typeReference = metadata.GetTypeReference(handle);
-
- if (metadata.GetString(typeReference.Name) == "Rules" &&
- metadata.GetString(typeReference.Namespace) == "TUnit.Analyzers")
- {
- rulesReferences.Add($"{metadata.GetString(typeReference.Namespace)}.{metadata.GetString(typeReference.Name)}");
- }
- }
+ var rulesReferences = RulesDecouplingVerifier.FindRulesTypeReferences(
+ typeof(MSTestMigrationCodeFixProvider).Assembly, "TUnit.Analyzers");
await TUnit.Assertions.Assert.That(rulesReferences)
.IsEmpty()
diff --git a/TUnit.Analyzers.Tests/TUnit.Analyzers.Tests.csproj b/TUnit.Analyzers.Tests/TUnit.Analyzers.Tests.csproj
index 1be1335763..bd0e79df7b 100644
--- a/TUnit.Analyzers.Tests/TUnit.Analyzers.Tests.csproj
+++ b/TUnit.Analyzers.Tests/TUnit.Analyzers.Tests.csproj
@@ -47,6 +47,8 @@
Visible="false" />
+