Skip to content
Open
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
61 changes: 61 additions & 0 deletions SharedTestHelpers/RulesDecouplingVerifier.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Verifies that a code fixer assembly carries no IL reference to its analyzer project's
/// <c>Rules</c> type. Guards against https://github.com/thomhurst/TUnit/issues/6157.
/// </summary>
/// <remarks>
/// Code fixer assemblies ship in the version-agnostic <c>analyzers/dotnet/cs</c> folder while the
/// analyzer assemblies ship per-Roslyn (<c>analyzers/dotnet/roslyn4.x/cs</c>), 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 <c>Rules</c> type — e.g.
/// <c>Rules.X.Id</c> inside the eagerly-evaluated <c>FixableDiagnosticIds</c> — then throws
/// <see cref="System.MissingFieldException"/> for rules the stale assembly doesn't have. Code
/// fixers must use the compile-time-baked <c>DiagnosticIds</c> constants instead, which this
/// helper enforces at the IL level: a <c>TypeReference</c> to <c>Rules</c> appears for any usage
/// (field access, <c>typeof</c>, method call), so an empty result proves full decoupling.
/// <c>DiagnosticIds</c> itself is also scanned — its members must stay <c>const</c>; changing one
/// to <c>static readonly</c> would silently reintroduce a runtime type reference, which surfaces
/// here as a <c>TypeReference</c> to <c>DiagnosticIds</c>.
/// <para>
/// Linked into each code fixer test project via
/// <c>&lt;Compile Include="..\SharedTestHelpers\RulesDecouplingVerifier.cs"&gt;</c>.
/// </para>
/// </remarks>
internal static class RulesDecouplingVerifier
{
/// <summary>
/// Returns the fully-qualified names of all <c>Rules</c> or <c>DiagnosticIds</c> type
/// references in <paramref name="codeFixersAssembly"/> whose namespace is
/// <paramref name="rulesNamespace"/>. An empty list means the assembly is fully decoupled.
/// </summary>
public static List<string> 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<string>();

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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@
public abstract class BaseMigrationCodeFixProvider : CodeFixProvider
{
protected abstract string FrameworkName { get; }

/// <summary>
/// The fixable diagnostic ID. Implementations MUST return a <see cref="DiagnosticIds"/> constant,
/// never <c>Rules.X.Id</c> — see <see cref="DiagnosticIds"/> remarks (issue #6157).
/// </summary>
protected abstract string DiagnosticId { get; }

protected abstract string CodeFixTitle { get; }

public sealed override ImmutableArray<string> FixableDiagnosticIds =>
Expand Down Expand Up @@ -208,7 +214,7 @@
compilationUnit = CleanupEndOfFileTrivia(compilationUnit);

// Normalize line endings to match original document (fixes cross-platform issues)
compilationUnit = NormalizeLineEndings(compilationUnit, root);

Check warning on line 217 in TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (fr-FR)

Existence possible d'un argument de référence null pour le paramètre 'originalRoot' dans 'CompilationUnitSyntax BaseMigrationCodeFixProvider.NormalizeLineEndings(CompilationUnitSyntax compilationUnit, SyntaxNode originalRoot)'.

Check warning on line 217 in TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (de-DE)

Mögliches Nullverweisargument für den Parameter "originalRoot" in "CompilationUnitSyntax BaseMigrationCodeFixProvider.NormalizeLineEndings(CompilationUnitSyntax compilationUnit, SyntaxNode originalRoot)".

Check warning on line 217 in TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (macos-latest)

Possible null reference argument for parameter 'originalRoot' in 'CompilationUnitSyntax BaseMigrationCodeFixProvider.NormalizeLineEndings(CompilationUnitSyntax compilationUnit, SyntaxNode originalRoot)'.

Check warning on line 217 in TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Possible null reference argument for parameter 'originalRoot' in 'CompilationUnitSyntax BaseMigrationCodeFixProvider.NormalizeLineEndings(CompilationUnitSyntax compilationUnit, SyntaxNode originalRoot)'.

Check warning on line 217 in TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (ubuntu-latest)

Possible null reference argument for parameter 'originalRoot' in 'CompilationUnitSyntax BaseMigrationCodeFixProvider.NormalizeLineEndings(CompilationUnitSyntax compilationUnit, SyntaxNode originalRoot)'.

// Add TODO comments for any failures so users know what needs manual attention
if (context.HasFailures)
Expand Down
8 changes: 5 additions & 3 deletions TUnit.Analyzers.CodeFixers/InheritsTestsCodeFixProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> FixableDiagnosticIds { get; } =
ImmutableArray.Create(Rules.DoesNotInheritTestsWarning.Id);
ImmutableArray.Create(DiagnosticIds.DoesNotInheritTestsWarning);

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

Expand All @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class MatrixDataSourceCodeFixProvider : CodeFixProvider
private const string Title = "Add [MatrixDataSource]";

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class TimeoutCancellationTokenCodeFixProvider : CodeFixProvider
private const string ParameterName = "cancellationToken";

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class VirtualHookOverrideCodeFixProvider : CodeFixProvider
private const string Title = "Remove redundant hook attribute";

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

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

Expand Down
4 changes: 2 additions & 2 deletions TUnit.Analyzers.CodeFixers/XUnitMigrationCodeFixProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
23 changes: 23 additions & 0 deletions TUnit.Analyzers.Tests/CodeFixerRulesDecouplingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using TUnit.Analyzers.CodeFixers;
using TUnit.Tests.Shared;

namespace TUnit.Analyzers.Tests;

/// <summary>
/// Code fixers must use <c>DiagnosticIds</c> constants, never <c>Rules.X</c> — see
/// <see cref="RulesDecouplingVerifier"/> for the full rationale (issue #6157).
/// </summary>
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)");
}
}
2 changes: 2 additions & 0 deletions TUnit.Analyzers.Tests/TUnit.Analyzers.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
Visible="false" />
<Compile Include="..\SharedTestHelpers\AnalyzerTestCompatibility.cs"
Link="Shared\AnalyzerTestCompatibility.cs" />
<Compile Include="..\SharedTestHelpers\RulesDecouplingVerifier.cs"
Link="Shared\RulesDecouplingVerifier.cs" />
</ItemGroup>

<!-- Reference the VSTHRD analyzer assembly (Analyzer="false") so its analyzer types are
Expand Down
68 changes: 68 additions & 0 deletions TUnit.Analyzers/DiagnosticIds.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
namespace TUnit.Analyzers;

/// <summary>
/// Diagnostic ID constants for all TUnit analyzer rules.
/// Code fix providers MUST reference these constants instead of <c>Rules.X.Id</c> — consts are
/// baked into the consuming IL at compile time, avoiding a runtime bind against a stale
/// analyzer assembly in Visual Studio. See https://github.com/thomhurst/TUnit/issues/6157.
/// Members MUST stay <c>const</c>: <c>static readonly</c> would reintroduce the runtime
/// reference (and fails the IL regression tests).
/// </summary>
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";
}
Loading
Loading