From 136cceab53ad47292e17d24484bbac2ac72a47a2 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Tue, 3 Mar 2026 21:37:32 +1100 Subject: [PATCH 1/2] ensure Ignore From Different Namespace Not Converted --- .../NUnitMigrationAnalyzerTests.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs b/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs index 0adaa7e858..e2111f83b5 100644 --- a/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs @@ -5460,6 +5460,59 @@ public void TestMethod() ); } + [Test] + public async Task NUnit_Ignore_From_Different_Namespace_Not_Converted() + { + await CodeFixer.VerifyCodeFixAsync( + """ + using NUnit.Framework; + + {|#0:public class MyClass|} + { + [Test] + [MyCompany.Attributes.Ignore("Not an NUnit attribute")] + public void MyMethod() + { + Assert.That(true, Is.True); + } + } + + namespace MyCompany.Attributes + { + public class IgnoreAttribute : System.Attribute + { + public IgnoreAttribute() { } + public IgnoreAttribute(string reason) { } + } + } + """, + Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0), + """ + using System.Threading.Tasks; + + public class MyClass + { + [Test] + [MyCompany.Attributes.Ignore("Not an NUnit attribute")] + public async Task MyMethod() + { + await Assert.That(true).IsTrue(); + } + } + + namespace MyCompany.Attributes + { + public class IgnoreAttribute : System.Attribute + { + public IgnoreAttribute() { } + public IgnoreAttribute(string reason) { } + } + } + """, + ConfigureNUnitTest + ); + } + [Test] public async Task NUnit_Global_Using_Flagged() { From fc9c5156b7f5f6caeb871134d13e3b0edac6a7ab Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Tue, 3 Mar 2026 22:38:41 +1100 Subject: [PATCH 2/2] . --- .../Base/TwoPhase/MigrationAnalyzer.cs | 30 +++++++++ .../TwoPhase/MSTestTwoPhaseAnalyzer.cs | 5 ++ .../TwoPhase/NUnitTwoPhaseAnalyzer.cs | 5 ++ .../TwoPhase/XUnitTwoPhaseAnalyzer.cs | 5 ++ .../MSTestMigrationAnalyzerTests.cs | 55 +++++++++++++++ .../NUnitMigrationAnalyzerTests.cs | 67 ++++++++++++------- .../XUnitMigrationAnalyzerTests.cs | 46 +++++++++++++ 7 files changed, 188 insertions(+), 25 deletions(-) diff --git a/TUnit.Analyzers.CodeFixers/Base/TwoPhase/MigrationAnalyzer.cs b/TUnit.Analyzers.CodeFixers/Base/TwoPhase/MigrationAnalyzer.cs index 7ddbc48bad..64a9268540 100644 --- a/TUnit.Analyzers.CodeFixers/Base/TwoPhase/MigrationAnalyzer.cs +++ b/TUnit.Analyzers.CodeFixers/Base/TwoPhase/MigrationAnalyzer.cs @@ -245,6 +245,10 @@ protected virtual CompilationUnitSyntax AnalyzeAttributes(CompilationUnitSyntax { try { + // Skip attributes that don't belong to the source framework + if (!IsFrameworkAttribute(originalNode)) + continue; + // Check for removal first if (ShouldRemoveAttribute(originalNode)) { @@ -306,6 +310,29 @@ protected virtual CompilationUnitSyntax AnalyzeAttributes(CompilationUnitSyntax /// protected abstract AttributeConversion? AnalyzeAttribute(AttributeSyntax node); + /// + /// Returns true if the given namespace belongs to the source framework being migrated. + /// Used to verify that attributes actually belong to the framework before converting them. + /// + protected abstract bool IsFrameworkNamespace(string? ns); + + /// + /// Returns true if the attribute belongs to the source framework being migrated, + /// verified via the semantic model. Returns true when the symbol cannot be resolved + /// to preserve existing behavior for unresolved types. + /// + protected bool IsFrameworkAttribute(AttributeSyntax node) + { + var symbolInfo = SemanticModel.GetSymbolInfo(node); + var symbol = symbolInfo.Symbol ?? symbolInfo.CandidateSymbols.FirstOrDefault(); + var ns = symbol?.ContainingType?.ContainingNamespace?.ToDisplayString(); + + if (ns == null) + return true; // If we can't resolve, assume framework (preserves existing behavior) + + return IsFrameworkNamespace(ns); + } + /// /// Analyzes parameter attributes (e.g., [Range] on method parameters). /// @@ -324,6 +351,9 @@ protected virtual CompilationUnitSyntax AnalyzeParameterAttributes(CompilationUn { try { + if (!IsFrameworkAttribute(originalAttr)) + continue; + var conversion = AnalyzeParameterAttribute(originalAttr, parameter); if (conversion != null) { diff --git a/TUnit.Analyzers.CodeFixers/TwoPhase/MSTestTwoPhaseAnalyzer.cs b/TUnit.Analyzers.CodeFixers/TwoPhase/MSTestTwoPhaseAnalyzer.cs index 322619d6e0..441c889c73 100644 --- a/TUnit.Analyzers.CodeFixers/TwoPhase/MSTestTwoPhaseAnalyzer.cs +++ b/TUnit.Analyzers.CodeFixers/TwoPhase/MSTestTwoPhaseAnalyzer.cs @@ -786,6 +786,11 @@ private static (string? message, bool hasComparer) GetMessageWithFormatArgs(Sepa return null; } + protected override bool IsFrameworkNamespace(string? ns) + { + return ns != null && ns.StartsWith("Microsoft.VisualStudio.TestTools.UnitTesting"); + } + protected override bool ShouldRemoveAttribute(AttributeSyntax node) { var name = MigrationHelpers.GetAttributeName(node); diff --git a/TUnit.Analyzers.CodeFixers/TwoPhase/NUnitTwoPhaseAnalyzer.cs b/TUnit.Analyzers.CodeFixers/TwoPhase/NUnitTwoPhaseAnalyzer.cs index 9374bc6b89..67d1741024 100644 --- a/TUnit.Analyzers.CodeFixers/TwoPhase/NUnitTwoPhaseAnalyzer.cs +++ b/TUnit.Analyzers.CodeFixers/TwoPhase/NUnitTwoPhaseAnalyzer.cs @@ -1355,6 +1355,11 @@ instanceOfAccess.Name is GenericNameSyntax instanceOfGeneric && return (AssertionConversionKind.NotEqual, assertion, true, null); } + protected override bool IsFrameworkNamespace(string? ns) + { + return ns != null && ns.StartsWith("NUnit.Framework"); + } + protected override bool ShouldRemoveAttribute(AttributeSyntax node) { var name = MigrationHelpers.GetAttributeName(node); diff --git a/TUnit.Analyzers.CodeFixers/TwoPhase/XUnitTwoPhaseAnalyzer.cs b/TUnit.Analyzers.CodeFixers/TwoPhase/XUnitTwoPhaseAnalyzer.cs index 68c50ac78b..54d737a04f 100644 --- a/TUnit.Analyzers.CodeFixers/TwoPhase/XUnitTwoPhaseAnalyzer.cs +++ b/TUnit.Analyzers.CodeFixers/TwoPhase/XUnitTwoPhaseAnalyzer.cs @@ -816,6 +816,11 @@ invocation.Expression is MemberAccessExpressionSyntax memberAccess && #region Attribute Analysis + protected override bool IsFrameworkNamespace(string? ns) + { + return ns != null && ns.StartsWith("Xunit"); + } + protected override bool ShouldRemoveAttribute(AttributeSyntax node) { var name = GetAttributeName(node); diff --git a/TUnit.Analyzers.Tests/MSTestMigrationAnalyzerTests.cs b/TUnit.Analyzers.Tests/MSTestMigrationAnalyzerTests.cs index 842e92b707..00267a6a32 100644 --- a/TUnit.Analyzers.Tests/MSTestMigrationAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/MSTestMigrationAnalyzerTests.cs @@ -1951,6 +1951,61 @@ await CodeFixer.VerifyCodeFixAsync( ); } + [Test] + [Arguments("TestClass")] + [Arguments("TestMethod")] + [Arguments("DataRow")] + [Arguments("DynamicData")] + [Arguments("TestInitialize")] + [Arguments("TestCleanup")] + [Arguments("TestCategory")] + [Arguments("Ignore")] + [Arguments("Priority")] + [Arguments("Owner")] + [Arguments("ExpectedException")] + public async Task MSTest_Attribute_From_Different_Namespace_Not_Converted(string attributeName) + { + await CodeFixer.VerifyCodeFixAsync( + $$""" + using Microsoft.VisualStudio.TestTools.UnitTesting; + + {|#0:public class MyClass|} + { + [Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod] + [MyCompany.Attributes.{{attributeName}}] + public void MyMethod1() { } + + [Microsoft.VisualStudio.TestTools.UnitTesting.TestMethod] + public void MyMethod2() { } + } + + namespace MyCompany.Attributes + { + public class {{attributeName}}Attribute : System.Attribute { } + } + """, + Verifier.Diagnostic(Rules.MSTestMigration).WithLocation(0), + $$""" + + public class MyClass + { + [Test] + [MyCompany.Attributes.{{attributeName}}] + public void MyMethod1() { } + + [Test] + public void MyMethod2() { } + } + + namespace MyCompany.Attributes + { + public class {{attributeName}}Attribute : System.Attribute { } + } + """, + ConfigureMSTestTest + ); + } + private static void ConfigureMSTestTest(Verifier.Test test) { test.TestState.AdditionalReferences.Add(typeof(TestMethodAttribute).Assembly); diff --git a/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs b/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs index e2111f83b5..625397e22a 100644 --- a/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs @@ -5461,52 +5461,69 @@ public void TestMethod() } [Test] - public async Task NUnit_Ignore_From_Different_Namespace_Not_Converted() + [Arguments("Test")] + [Arguments("Theory")] + [Arguments("TestCase")] + [Arguments("TestCaseSource")] + [Arguments("SetUp")] + [Arguments("TearDown")] + [Arguments("OneTimeSetUp")] + [Arguments("OneTimeTearDown")] + [Arguments("TestFixture")] + [Arguments("Category")] + [Arguments("Ignore")] + [Arguments("Explicit")] + [Arguments("Description")] + [Arguments("Author")] + [Arguments("Apartment")] + [Arguments("Parallelizable")] + [Arguments("NonParallelizable")] + [Arguments("Repeat")] + [Arguments("Values")] + [Arguments("Range")] + [Arguments("ValueSource")] + [Arguments("Sequential")] + [Arguments("Combinatorial")] + [Arguments("Platform")] + [Arguments("ExpectedException")] + [Arguments("FixtureLifeCycle")] + public async Task NUnit_Attribute_From_Different_Namespace_Not_Converted(string attributeName) { await CodeFixer.VerifyCodeFixAsync( - """ + $$""" using NUnit.Framework; {|#0:public class MyClass|} { - [Test] - [MyCompany.Attributes.Ignore("Not an NUnit attribute")] - public void MyMethod() - { - Assert.That(true, Is.True); - } + [NUnit.Framework.Test] + [MyCompany.Attributes.{{attributeName}}] + public void MyMethod1() { } + + [NUnit.Framework.Test] + public void MyMethod2() { } } namespace MyCompany.Attributes { - public class IgnoreAttribute : System.Attribute - { - public IgnoreAttribute() { } - public IgnoreAttribute(string reason) { } - } + public class {{attributeName}}Attribute : System.Attribute { } } """, Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0), - """ - using System.Threading.Tasks; + $$""" public class MyClass { [Test] - [MyCompany.Attributes.Ignore("Not an NUnit attribute")] - public async Task MyMethod() - { - await Assert.That(true).IsTrue(); - } + [MyCompany.Attributes.{{attributeName}}] + public void MyMethod1() { } + + [Test] + public void MyMethod2() { } } namespace MyCompany.Attributes { - public class IgnoreAttribute : System.Attribute - { - public IgnoreAttribute() { } - public IgnoreAttribute(string reason) { } - } + public class {{attributeName}}Attribute : System.Attribute { } } """, ConfigureNUnitTest diff --git a/TUnit.Analyzers.Tests/XUnitMigrationAnalyzerTests.cs b/TUnit.Analyzers.Tests/XUnitMigrationAnalyzerTests.cs index 7b6a603d63..6d63f1763e 100644 --- a/TUnit.Analyzers.Tests/XUnitMigrationAnalyzerTests.cs +++ b/TUnit.Analyzers.Tests/XUnitMigrationAnalyzerTests.cs @@ -2620,6 +2620,52 @@ private static void ConfigureXUnitGlobalUsingTest(CodeFixer.Test test) """))); } + [Test] + [Arguments("Fact")] + [Arguments("Theory")] + [Arguments("InlineData")] + [Arguments("MemberData")] + [Arguments("ClassData")] + [Arguments("Trait")] + [Arguments("Collection")] + [Arguments("CollectionDefinition")] + public async Task XUnit_Attribute_From_Different_Namespace_Not_Converted(string attributeName) + { + await CodeFixer.VerifyCodeFixAsync( + $$""" + {|#0:using Xunit; + + public class MyClass + { + [Xunit.Fact] + [MyCompany.Attributes.{{attributeName}}] + public void MyMethod() { } + } + + namespace MyCompany.Attributes + { + public class {{attributeName}}Attribute : System.Attribute { } + }|} + """, + Verifier.Diagnostic(Rules.XunitMigration).WithLocation(0), + $$""" + + public class MyClass + { + [Test] + [MyCompany.Attributes.{{attributeName}}] + public void MyMethod() { } + } + + namespace MyCompany.Attributes + { + public class {{attributeName}}Attribute : System.Attribute { } + } + """, + ConfigureXUnitTest + ); + } + private static void ConfigureXUnitTest(Verifier.Test test) { var globalUsings = ("GlobalUsings.cs", SourceText.From("global using Xunit;"));