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 eedfc5a6b3..44d36a0600 100644
--- a/TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs
+++ b/TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs
@@ -13,7 +13,13 @@ 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 — see remarks (issue #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..c7cd027112
--- /dev/null
+++ b/TUnit.Analyzers.Tests/CodeFixerRulesDecouplingTests.cs
@@ -0,0 +1,23 @@
+using TUnit.Analyzers.CodeFixers;
+using TUnit.Tests.Shared;
+
+namespace TUnit.Analyzers.Tests;
+
+///
+/// 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 rulesReferences = RulesDecouplingVerifier.FindRulesTypeReferences(
+ typeof(MSTestMigrationCodeFixProvider).Assembly, "TUnit.Analyzers");
+
+ 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.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" />
+