diff --git a/Documentation/Diagnostics/PH2152.md b/Documentation/Diagnostics/PH2152.md
new file mode 100644
index 000000000..5125830e5
--- /dev/null
+++ b/Documentation/Diagnostics/PH2152.md
@@ -0,0 +1,69 @@
+# PH2152: Order DataRow attribute above TestMethod for unit tests
+
+| Property | Value |
+|--|--|
+| Package | [Philips.CodeAnalysis.MsTestAnalyzers](https://www.nuget.org/packages/Philips.CodeAnalysis.MsTestAnalyzers) |
+| Diagnostic ID | PH2152 |
+| Category | [MsTest](../MsTest.md) |
+| Analyzer | [DataRowOrderAnalyzer](https://github.com/philips-software/roslyn-analyzers/blob/main/Philips.CodeAnalysis.MsTestAnalyzers/DataRowOrderAnalyzer.cs)
+| CodeFix | Yes |
+| Severity | Warning |
+| Enabled By Default | No |
+
+## Introduction
+
+DataRow attributes should be consistently ordered above TestMethod/DataTestMethod attributes on test methods to improve readability and maintain consistency across the codebase.
+
+## How to solve
+
+Move all DataRow attributes to appear before TestMethod or DataTestMethod attributes on the method.
+
+## Example
+
+Code that triggers a diagnostic:
+``` cs
+[TestClass]
+public class Tests
+{
+ [TestMethod]
+ [DataRow(1, 2)]
+ [DataRow(3, 4)]
+ public void TestMethod1(int x, int y)
+ {
+ // Test implementation
+ }
+
+ [DataTestMethod]
+ [DataRow("test")]
+ public void TestMethod2(string value)
+ {
+ // Test implementation
+ }
+}
+```
+
+And the replacement code:
+``` cs
+[TestClass]
+public class Tests
+{
+ [DataRow(1, 2)]
+ [DataRow(3, 4)]
+ [TestMethod]
+ public void TestMethod1(int x, int y)
+ {
+ // Test implementation
+ }
+
+ [DataRow("test")]
+ [DataTestMethod]
+ public void TestMethod2(string value)
+ {
+ // Test implementation
+ }
+}
+```
+
+## Configuration
+
+This analyzer does not offer any special configuration. The general ways of [suppressing](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/suppress-warnings) diagnostics apply.
\ No newline at end of file
diff --git a/Philips.CodeAnalysis.Common/AttributeHelper.cs b/Philips.CodeAnalysis.Common/AttributeHelper.cs
index f1d3b46a4..0e2e7a43c 100644
--- a/Philips.CodeAnalysis.Common/AttributeHelper.cs
+++ b/Philips.CodeAnalysis.Common/AttributeHelper.cs
@@ -1,6 +1,7 @@
// © 2019 Koninklijke Philips N.V. See License.md in the project root for license information.
using System;
+using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
@@ -122,6 +123,88 @@ public bool IsDataRowAttribute(AttributeSyntax attribute, SyntaxNodeAnalysisCont
return IsAttribute(attribute, context, MsTestFrameworkDefinitions.DataRowAttribute, out _, out _);
}
+ ///
+ /// Checks if any attribute of the specified types appears after any attribute of the other specified types.
+ ///
+ /// The attribute lists to examine
+ /// The syntax node analysis context
+ /// The attributes to look for that should appear first
+ /// The attributes that should not appear before attributesToFind
+ /// True if any attributesToFind appears after any attributesToCheckAfter
+ public bool HasAttributeAfterOther(SyntaxList attributeLists, SyntaxNodeAnalysisContext context, INamedTypeSymbol[] attributesToFind, INamedTypeSymbol[] attributesToCheckAfter)
+ {
+ var allAttributes = attributeLists
+ .SelectMany((list, listIndex) =>
+ list.Attributes.Select((attr, attrIndex) => new
+ {
+ Attribute = attr,
+ Position = (listIndex, attrIndex),
+ Symbol = context.SemanticModel.GetSymbolInfo(attr).Symbol?.ContainingType
+ }))
+ .Where(x => x.Symbol != null)
+ .ToArray();
+
+ (int listIndex, int attrIndex)? firstCheckAfterPosition = allAttributes
+ .Where(x => attributesToCheckAfter.Any(attr => AttributeMatches(x.Symbol, attr)))
+ .Select(x => x.Position)
+ .FirstOrDefault();
+
+ if (firstCheckAfterPosition == null)
+ {
+ return false;
+ }
+
+ return allAttributes
+ .Where(x => attributesToFind.Any(attr => AttributeMatches(x.Symbol, attr)))
+ .Any(x => x.Position.CompareTo(firstCheckAfterPosition.Value) > 0);
+ }
+
+ ///
+ /// Categorizes attributes into groups based on the provided type predicates.
+ ///
+ /// The attribute lists to categorize
+ /// The syntax node analysis context
+ /// Functions that determine which category an attribute belongs to
+ /// A dictionary mapping category names to lists of attributes
+ public Dictionary> CategorizeAttributes(SyntaxList attributeLists, SyntaxNodeAnalysisContext context, Dictionary> categorizers)
+ {
+ var result = new Dictionary>();
+ foreach (var category in categorizers.Keys)
+ {
+ result[category] = [];
+ }
+
+ foreach (AttributeListSyntax attributeList in attributeLists)
+ {
+ foreach (AttributeSyntax attribute in attributeList.Attributes)
+ {
+ INamedTypeSymbol attributeSymbol = context.SemanticModel.GetSymbolInfo(attribute).Symbol?.ContainingType;
+ if (attributeSymbol != null)
+ {
+ KeyValuePair>? matchingCategorizer = categorizers.Where(c => c.Value(attributeSymbol)).FirstOrDefault();
+ if (matchingCategorizer.HasValue)
+ {
+ result[matchingCategorizer.Value.Key].Add(attribute);
+ }
+ }
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Checks if an attribute symbol matches a target symbol (including inheritance).
+ ///
+ /// The attribute symbol to check
+ /// The target symbol to match against
+ /// True if the symbols match or if attributeSymbol is derived from targetSymbol
+ private static bool AttributeMatches(INamedTypeSymbol attributeSymbol, INamedTypeSymbol targetSymbol)
+ {
+ return SymbolEqualityComparer.Default.Equals(attributeSymbol, targetSymbol) ||
+ attributeSymbol.IsDerivedFrom(targetSymbol);
+ }
+
public bool TryExtractAttributeArgument(AttributeArgumentSyntax argumentSyntax, SyntaxNodeAnalysisContext context, out string argumentString, out T value)
{
argumentString = argumentSyntax.Expression.ToString();
diff --git a/Philips.CodeAnalysis.Common/DiagnosticId.cs b/Philips.CodeAnalysis.Common/DiagnosticId.cs
index 890ae1fa7..b959282da 100644
--- a/Philips.CodeAnalysis.Common/DiagnosticId.cs
+++ b/Philips.CodeAnalysis.Common/DiagnosticId.cs
@@ -141,6 +141,7 @@ public enum DiagnosticId
AvoidVariableNamedUnderscore = 2147,
AvoidProblematicUsingPatterns = 2149,
AvoidTodoComments = 2151,
+ DataRowOrderInTestMethod = 2152,
AvoidUnusedToString = 2153,
AvoidUnlicensedPackages = 2155,
AvoidPkcsPaddingWithRsaEncryption = 2158,
diff --git a/Philips.CodeAnalysis.Common/Helper.cs b/Philips.CodeAnalysis.Common/Helper.cs
index 82ffa45f2..8cdd5f639 100644
--- a/Philips.CodeAnalysis.Common/Helper.cs
+++ b/Philips.CodeAnalysis.Common/Helper.cs
@@ -10,5 +10,7 @@ public class Helper(AnalyzerOptions options, Compilation compilation) : CodeFixH
public AllowedSymbols ForAllowedSymbols { get; } = new AllowedSymbols(compilation);
public AdditionalFilesHelper ForAdditionalFiles { get; } = new AdditionalFilesHelper(options, compilation);
+
+ public AttributeHelper AttributeHelper { get; } = new AttributeHelper();
}
}
diff --git a/Philips.CodeAnalysis.MsTestAnalyzers/DataRowOrderAnalyzer.cs b/Philips.CodeAnalysis.MsTestAnalyzers/DataRowOrderAnalyzer.cs
new file mode 100644
index 000000000..b32be6ddd
--- /dev/null
+++ b/Philips.CodeAnalysis.MsTestAnalyzers/DataRowOrderAnalyzer.cs
@@ -0,0 +1,68 @@
+// © 2025 Koninklijke Philips N.V. See License.md in the project root for license information.
+
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Philips.CodeAnalysis.Common;
+
+namespace Philips.CodeAnalysis.MsTestAnalyzers
+{
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public class DataRowOrderAnalyzer : TestAttributeDiagnosticAnalyzer
+ {
+ private const string Title = @"Order DataRow attribute above TestMethod for unit tests";
+ public static readonly string MessageFormat = @"DataRow attributes should be placed above TestMethod/DataTestMethod attributes";
+ private const string Description = @"DataRow attributes should be consistently ordered above TestMethod/DataTestMethod attributes on test methods";
+ private const string Category = Categories.MsTest;
+
+ private static readonly DiagnosticDescriptor Rule = new(DiagnosticId.DataRowOrderInTestMethod.ToId(),
+ Title, MessageFormat, Category, DiagnosticSeverity.Warning, isEnabledByDefault: false, description: Description);
+
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+ protected override Implementation OnInitializeAnalyzer(AnalyzerOptions options, Compilation compilation, MsTestAttributeDefinitions definitions)
+ {
+ return new DataRowOrderImplementation(definitions, Helper);
+ }
+
+ private sealed class DataRowOrderImplementation : Implementation
+ {
+ private readonly MsTestAttributeDefinitions _definitions;
+
+ public DataRowOrderImplementation(MsTestAttributeDefinitions definitions, Helper helper) : base(helper)
+ {
+ _definitions = definitions;
+ }
+
+ public override void OnTestAttributeMethod(SyntaxNodeAnalysisContext context, MethodDeclarationSyntax methodDeclaration, IMethodSymbol methodSymbol, HashSet presentAttributes)
+ {
+ if (!HasRequiredAttributes(presentAttributes))
+ {
+ return;
+ }
+
+ // Check if DataRow comes after TestMethod using the generic AttributeHelper method
+ INamedTypeSymbol[] dataRowAttributes = { _definitions.DataRowSymbol };
+ INamedTypeSymbol[] testMethodAttributes = { _definitions.TestMethodSymbol, _definitions.DataTestMethodSymbol };
+
+ if (Helper.AttributeHelper.HasAttributeAfterOther(methodDeclaration.AttributeLists, context, dataRowAttributes, testMethodAttributes))
+ {
+ Location location = methodDeclaration.Identifier.GetLocation();
+ context.ReportDiagnostic(Diagnostic.Create(Rule, location, methodDeclaration.Identifier));
+ }
+ }
+
+ private bool HasRequiredAttributes(HashSet presentAttributes)
+ {
+ return presentAttributes.Contains(_definitions.DataRowSymbol) &&
+ presentAttributes.Any(attr =>
+ attr.IsDerivedFrom(_definitions.TestMethodSymbol) ||
+ attr.IsDerivedFrom(_definitions.DataTestMethodSymbol));
+ }
+ }
+ }
+}
diff --git a/Philips.CodeAnalysis.MsTestAnalyzers/DataRowOrderCodeFixProvider.cs b/Philips.CodeAnalysis.MsTestAnalyzers/DataRowOrderCodeFixProvider.cs
new file mode 100644
index 000000000..f05374678
--- /dev/null
+++ b/Philips.CodeAnalysis.MsTestAnalyzers/DataRowOrderCodeFixProvider.cs
@@ -0,0 +1,100 @@
+// © 2025 Koninklijke Philips N.V. See License.md in the project root for license information.
+
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Composition;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Philips.CodeAnalysis.Common;
+
+namespace Philips.CodeAnalysis.MsTestAnalyzers
+{
+ [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(DataRowOrderCodeFixProvider)), Shared]
+ public class DataRowOrderCodeFixProvider : SingleDiagnosticCodeFixProvider
+ {
+ protected override string Title => "Move DataRow attributes above TestMethod";
+
+ protected override DiagnosticId DiagnosticId => DiagnosticId.DataRowOrderInTestMethod;
+
+ protected override async Task ApplyFix(Document document, MethodDeclarationSyntax node, ImmutableDictionary properties, CancellationToken cancellationToken)
+ {
+ SyntaxNode root = await document.GetSyntaxRootAsync(cancellationToken);
+ SemanticModel semanticModel = await document.GetSemanticModelAsync(cancellationToken);
+
+ // Get the MsTest definitions to identify attributes
+ var definitions = MsTestAttributeDefinitions.FromCompilation(semanticModel.Compilation);
+
+ // Reorder the attributes
+ MethodDeclarationSyntax reorderedMethod = ReorderAttributes(node, semanticModel, definitions);
+
+ SyntaxNode newRoot = root.ReplaceNode(node, reorderedMethod);
+ return document.WithSyntaxRoot(newRoot);
+ }
+
+ private static MethodDeclarationSyntax ReorderAttributes(MethodDeclarationSyntax method, SemanticModel semanticModel, MsTestAttributeDefinitions definitions)
+ {
+ var dataRowAttributes = new List();
+ var testMethodAttributes = new List();
+ var otherAttributes = new List();
+
+ // Categorize all attributes
+ foreach (AttributeListSyntax attributeList in method.AttributeLists)
+ {
+ foreach (AttributeSyntax attribute in attributeList.Attributes)
+ {
+ INamedTypeSymbol attributeSymbol = semanticModel.GetSymbolInfo(attribute).Symbol?.ContainingType;
+ if (attributeSymbol != null)
+ {
+ if (SymbolEqualityComparer.Default.Equals(attributeSymbol, definitions.DataRowSymbol))
+ {
+ dataRowAttributes.Add(attribute);
+ }
+ else if (attributeSymbol.IsDerivedFrom(definitions.TestMethodSymbol) ||
+ attributeSymbol.IsDerivedFrom(definitions.DataTestMethodSymbol))
+ {
+ testMethodAttributes.Add(attribute);
+ }
+ else
+ {
+ otherAttributes.Add(attribute);
+ }
+ }
+ else
+ {
+ otherAttributes.Add(attribute);
+ }
+ }
+ }
+
+ // Create new attribute lists in the correct order: DataRow, Other, TestMethod
+ SyntaxList newAttributeLists = SyntaxFactory.List();
+
+ // Add DataRow attributes first
+ foreach (AttributeSyntax attr in dataRowAttributes)
+ {
+ newAttributeLists = newAttributeLists.Add(
+ SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(attr)));
+ }
+
+ // Add other attributes (like TestCategory, Timeout, etc.)
+ foreach (AttributeSyntax attr in otherAttributes)
+ {
+ newAttributeLists = newAttributeLists.Add(
+ SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(attr)));
+ }
+
+ // Add TestMethod/DataTestMethod attributes last
+ foreach (AttributeSyntax attr in testMethodAttributes)
+ {
+ newAttributeLists = newAttributeLists.Add(
+ SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(attr)));
+ }
+
+ return method.WithAttributeLists(newAttributeLists);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Philips.CodeAnalysis.MsTestAnalyzers/Philips.CodeAnalysis.MsTestAnalyzers.md b/Philips.CodeAnalysis.MsTestAnalyzers/Philips.CodeAnalysis.MsTestAnalyzers.md
index e56f13613..1d6ec3c98 100644
--- a/Philips.CodeAnalysis.MsTestAnalyzers/Philips.CodeAnalysis.MsTestAnalyzers.md
+++ b/Philips.CodeAnalysis.MsTestAnalyzers/Philips.CodeAnalysis.MsTestAnalyzers.md
@@ -38,5 +38,5 @@
| [PH2059](../Documentation/Diagnostics/PH2059.md) | Public Method should be TestMethod | Public methods inside a TestClass should either be a test method or non-public. | ✅ **Use [MSTEST0029](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0029)** |
| [PH2076](../Documentation/Diagnostics/PH2076.md) | Assert.Fail alternatives | Assert.Fail should not be used if an alternative is more appropriate | ⚠️ **Consider [MSTEST0025](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0025)** |
| [PH2095](../Documentation/Diagnostics/PH2095.md) | TestMethods must return void/Task for async methods | TestMethods must return Task if they are async methods, or void if not | ⚠️ **Consider [MSTEST0003](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0003)** |
-
+| [PH2152](../Documentation/Diagnostics/PH2152.md) | Order DataRow attribute above TestMethod for unit tests | DataRow attributes should be consistently ordered above TestMethod/DataTestMethod attributes on test methods |
diff --git a/Philips.CodeAnalysis.Test/MsTest/DataRowOrderAnalyzerTest.cs b/Philips.CodeAnalysis.Test/MsTest/DataRowOrderAnalyzerTest.cs
new file mode 100644
index 000000000..92764a76f
--- /dev/null
+++ b/Philips.CodeAnalysis.Test/MsTest/DataRowOrderAnalyzerTest.cs
@@ -0,0 +1,190 @@
+// © 2025 Koninklijke Philips N.V. See License.md in the project root for license information.
+
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Philips.CodeAnalysis.Common;
+using Philips.CodeAnalysis.MsTestAnalyzers;
+using Philips.CodeAnalysis.Test.Helpers;
+using Philips.CodeAnalysis.Test.Verifiers;
+
+namespace Philips.CodeAnalysis.Test.MsTest
+{
+ [TestClass]
+ public class DataRowOrderAnalyzerTest : DiagnosticVerifier
+ {
+ protected override DiagnosticAnalyzer GetDiagnosticAnalyzer()
+ {
+ return new DataRowOrderAnalyzer();
+ }
+
+ private static DiagnosticResult CreateExpectedResult(int line, int column)
+ {
+ return new DiagnosticResult
+ {
+ Id = DiagnosticId.DataRowOrderInTestMethod.ToId(),
+ Severity = DiagnosticSeverity.Warning,
+ Location = new DiagnosticResultLocation(null, line, column),
+ Message = new System.Text.RegularExpressions.Regex(".*")
+ };
+ }
+
+ [TestMethod]
+ [TestCategory(TestDefinitions.UnitTests)]
+ public async Task GoodOrderNoDiagnosticAsync()
+ {
+ const string code = @"
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class Tests
+{
+ [DataRow(1, 2)]
+ [DataRow(3, 4)]
+ [TestMethod]
+ public void TestMethod1(int x, int y) { }
+
+ [DataRow(""test"")]
+ [DataTestMethod]
+ public void TestMethod2(string value) { }
+}";
+
+ await VerifySuccessfulCompilation(code).ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ [TestCategory(TestDefinitions.UnitTests)]
+ public async Task BadOrderReportsDiagnosticAsync()
+ {
+ const string code = @"
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class Tests
+{
+ [TestMethod]
+ [DataRow(1, 2)]
+ public void TestMethod1(int x, int y) { }
+}";
+
+ await VerifyDiagnostic(code, CreateExpectedResult(9, 14)).ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ [TestCategory(TestDefinitions.UnitTests)]
+ public async Task NoDataRowNoDiagnosticAsync()
+ {
+ const string code = @"
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class Tests
+{
+ [TestMethod]
+ public void TestMethod1() { }
+
+ [DataTestMethod]
+ public void TestMethod2() { }
+}";
+
+ await VerifySuccessfulCompilation(code).ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ [TestCategory(TestDefinitions.UnitTests)]
+ public async Task NoTestMethodNoDiagnosticAsync()
+ {
+ const string code = @"
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class Tests
+{
+ [DataRow(1, 2)]
+ public void TestMethod1(int x, int y) { }
+}";
+
+ await VerifySuccessfulCompilation(code).ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ [TestCategory(TestDefinitions.UnitTests)]
+ public async Task MixedWithOtherAttributesGoodOrderNoDiagnosticAsync()
+ {
+ const string code = @"
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class Tests
+{
+ [DataRow(1, 2)]
+ [TestCategory(""Unit"")]
+ [TestMethod]
+ [Timeout(1000)]
+ public void TestMethod1(int x, int y) { }
+}";
+
+ await VerifySuccessfulCompilation(code).ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ [TestCategory(TestDefinitions.UnitTests)]
+ public async Task MixedWithOtherAttributesBadOrderReportsDiagnosticAsync()
+ {
+ const string code = @"
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class Tests
+{
+ [TestCategory(""Unit"")]
+ [TestMethod]
+ [DataRow(1, 2)]
+ [Timeout(1000)]
+ public void TestMethod1(int x, int y) { }
+}";
+
+ await VerifyDiagnostic(code, CreateExpectedResult(11, 14)).ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ [TestCategory(TestDefinitions.UnitTests)]
+ public async Task MultipleDataRowsGoodOrderNoDiagnosticAsync()
+ {
+ const string code = @"
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class Tests
+{
+ [DataRow(1, 2)]
+ [DataRow(3, 4)]
+ [DataRow(5, 6)]
+ [TestMethod]
+ public void TestMethod1(int x, int y) { }
+}";
+
+ await VerifySuccessfulCompilation(code).ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ [TestCategory(TestDefinitions.UnitTests)]
+ public async Task MultipleDataRowsBadOrderReportsDiagnosticAsync()
+ {
+ const string code = @"
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class Tests
+{
+ [DataRow(1, 2)]
+ [TestMethod]
+ [DataRow(3, 4)]
+ public void TestMethod1(int x, int y) { }
+}";
+
+ await VerifyDiagnostic(code, CreateExpectedResult(10, 14)).ConfigureAwait(false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Philips.CodeAnalysis.Test/MsTest/DataRowOrderCodeFixProviderTest.cs b/Philips.CodeAnalysis.Test/MsTest/DataRowOrderCodeFixProviderTest.cs
new file mode 100644
index 000000000..0df4fce1a
--- /dev/null
+++ b/Philips.CodeAnalysis.Test/MsTest/DataRowOrderCodeFixProviderTest.cs
@@ -0,0 +1,201 @@
+// © 2025 Koninklijke Philips N.V. See License.md in the project root for license information.
+
+using System.Threading.Tasks;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.Diagnostics;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Philips.CodeAnalysis.Common;
+using Philips.CodeAnalysis.MsTestAnalyzers;
+using Philips.CodeAnalysis.Test.Helpers;
+using Philips.CodeAnalysis.Test.Verifiers;
+
+namespace Philips.CodeAnalysis.Test.MsTest
+{
+ [TestClass]
+ public class DataRowOrderCodeFixProviderTest : CodeFixVerifier
+ {
+ protected override DiagnosticAnalyzer GetDiagnosticAnalyzer()
+ {
+ return new DataRowOrderAnalyzer();
+ }
+
+ protected override CodeFixProvider GetCodeFixProvider()
+ {
+ return new DataRowOrderCodeFixProvider();
+ }
+
+ private static DiagnosticResult CreateExpectedResult(int line, int column)
+ {
+ return new DiagnosticResult
+ {
+ Id = DiagnosticId.DataRowOrderInTestMethod.ToId(),
+ Severity = DiagnosticSeverity.Warning,
+ Location = new DiagnosticResultLocation(null, line, column),
+ Message = new System.Text.RegularExpressions.Regex(".*")
+ };
+ }
+
+ [TestMethod]
+ [TestCategory(TestDefinitions.UnitTests)]
+ public async Task SimpleReorderingCodeFixAsync()
+ {
+ const string given = @"
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class Tests
+{
+ [TestMethod]
+ [DataRow(1, 2)]
+ public void TestMethod1(int x, int y) { }
+}";
+
+ const string expected = @"
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class Tests
+{
+ [DataRow(1, 2)]
+ [TestMethod]
+ public void TestMethod1(int x, int y) { }
+}";
+
+ await VerifyDiagnostic(given, CreateExpectedResult(9, 14)).ConfigureAwait(false);
+ await VerifyFix(given, expected).ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ [TestCategory(TestDefinitions.UnitTests)]
+ public async Task MultipleDataRowsCodeFixAsync()
+ {
+ const string given = @"
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class Tests
+{
+ [TestMethod]
+ [DataRow(1, 2)]
+ [DataRow(3, 4)]
+ public void TestMethod1(int x, int y) { }
+}";
+
+ const string expected = @"
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class Tests
+{
+ [DataRow(1, 2)]
+ [DataRow(3, 4)]
+ [TestMethod]
+ public void TestMethod1(int x, int y) { }
+}";
+
+ await VerifyDiagnostic(given, CreateExpectedResult(10, 14)).ConfigureAwait(false);
+ await VerifyFix(given, expected).ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ [TestCategory(TestDefinitions.UnitTests)]
+ public async Task WithOtherAttributesCodeFixAsync()
+ {
+ const string given = @"
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class Tests
+{
+ [TestCategory(""Unit"")]
+ [TestMethod]
+ [DataRow(1, 2)]
+ [Timeout(1000)]
+ public void TestMethod1(int x, int y) { }
+}";
+
+ const string expected = @"
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class Tests
+{
+ [DataRow(1, 2)]
+ [TestCategory(""Unit"")]
+ [Timeout(1000)]
+ [TestMethod]
+ public void TestMethod1(int x, int y) { }
+}";
+
+ await VerifyDiagnostic(given, CreateExpectedResult(11, 14)).ConfigureAwait(false);
+ await VerifyFix(given, expected).ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ [TestCategory(TestDefinitions.UnitTests)]
+ public async Task DataTestMethodCodeFixAsync()
+ {
+ const string given = @"
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class Tests
+{
+ [DataTestMethod]
+ [DataRow(""test"")]
+ public void TestMethod1(string value) { }
+}";
+
+ const string expected = @"
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class Tests
+{
+ [DataRow(""test"")]
+ [DataTestMethod]
+ public void TestMethod1(string value) { }
+}";
+
+ await VerifyDiagnostic(given, CreateExpectedResult(9, 14)).ConfigureAwait(false);
+ await VerifyFix(given, expected).ConfigureAwait(false);
+ }
+
+ [TestMethod]
+ [TestCategory(TestDefinitions.UnitTests)]
+ public async Task ComplexMixedAttributesCodeFixAsync()
+ {
+ const string given = @"
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class Tests
+{
+ [TestCategory(""Unit"")]
+ [DataRow(1, 2)]
+ [TestMethod]
+ [DataRow(3, 4)]
+ [Timeout(1000)]
+ public void TestMethod1(int x, int y) { }
+}";
+
+ const string expected = @"
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class Tests
+{
+ [DataRow(1, 2)]
+ [DataRow(3, 4)]
+ [TestCategory(""Unit"")]
+ [Timeout(1000)]
+ [TestMethod]
+ public void TestMethod1(int x, int y) { }
+}";
+
+ await VerifyDiagnostic(given, CreateExpectedResult(12, 14)).ConfigureAwait(false);
+ await VerifyFix(given, expected).ConfigureAwait(false);
+ }
+ }
+}
\ No newline at end of file