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
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,15 @@
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>): VS evaluates <see cref="FixableDiagnosticIds"/> 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.
/// </summary>
protected abstract string DiagnosticId { get; }

protected abstract string CodeFixTitle { get; }

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

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

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

View workflow job for this annotation

GitHub Actions / modularpipeline (pl-PL)

Możliwy argument odwołania o wartości null dla parametru „originalRoot” w „CompilationUnitSyntax BaseMigrationCodeFixProvider.NormalizeLineEndings(CompilationUnitSyntax compilationUnit, SyntaxNode originalRoot)”.

Check warning on line 219 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 219 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 219 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 219 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 219 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 219 in TUnit.Analyzers.CodeFixers/Base/BaseMigrationCodeFixProvider.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-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 @@
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 Expand Up @@ -331,7 +331,7 @@
}

var osNames = new List<string>();
var platforms = platformString.Split(',');

Check warning on line 334 in TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (pl-PL)

Wyłuskanie odwołania, które może mieć wartość null.

Check warning on line 334 in TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs

View workflow job for this annotation

GitHub Actions / modularpipeline (windows-latest)

Dereference of a possibly null reference.

foreach (var platform in platforms)
{
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
47 changes: 47 additions & 0 deletions TUnit.Analyzers.Tests/CodeFixerRulesDecouplingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using TUnit.Analyzers.CodeFixers;

namespace TUnit.Analyzers.Tests;

/// <summary>
/// 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
/// <c>Rules</c> type (e.g. <c>Rules.MSTestMigration.Id</c> inside the eagerly-evaluated
/// <c>FixableDiagnosticIds</c>) then throws MissingFieldException for rules the stale assembly
/// doesn't have. Code fixers must use the compile-time-baked <c>DiagnosticIds</c> constants instead.
/// </summary>
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<string>();

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)");
}
}
71 changes: 71 additions & 0 deletions TUnit.Analyzers/DiagnosticIds.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
namespace TUnit.Analyzers;

/// <summary>
/// Diagnostic ID constants for all TUnit analyzer rules.
/// </summary>
/// <remarks>
/// Code fix providers (TUnit.Analyzers.CodeFixers) MUST reference these constants instead of
/// <c>Rules.X.Id</c>. Constants are baked into the consuming assembly at compile time, so the
/// code fixers carry no runtime reference to the <see cref="Rules"/> type. A runtime reference
/// can bind against a stale TUnit.Analyzers.dll already loaded in Visual Studio (analyzers
/// cannot be unloaded), throwing <see cref="System.MissingFieldException"/> for newly added
/// rules. See https://github.com/thomhurst/TUnit/issues/6157.
/// </remarks>
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