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 0adaa7e858..625397e22a 100644
--- a/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs
+++ b/TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs
@@ -5460,6 +5460,76 @@ public void TestMethod()
);
}
+ [Test]
+ [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|}
+ {
+ [NUnit.Framework.Test]
+ [MyCompany.Attributes.{{attributeName}}]
+ public void MyMethod1() { }
+
+ [NUnit.Framework.Test]
+ public void MyMethod2() { }
+ }
+
+ namespace MyCompany.Attributes
+ {
+ public class {{attributeName}}Attribute : System.Attribute { }
+ }
+ """,
+ Verifier.Diagnostic(Rules.NUnitMigration).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 { }
+ }
+ """,
+ ConfigureNUnitTest
+ );
+ }
+
[Test]
public async Task NUnit_Global_Using_Flagged()
{
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;"));