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;"));