-
Notifications
You must be signed in to change notification settings - Fork 14
feat: Add DataRow ordering analyzer with code fixer and generic attribute ordering utilities (PH2152) #874
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6abe53f
3b14036
a5316ef
c40717b
45303fc
d86acc3
b86e5f0
7167510
fd067b9
ef95169
504c63a
7d17055
c90708a
094fe9a
8fa6765
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| // © 2025 Koninklijke Philips N.V. See License.md in the project root for license information. | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not use any #pragmas. fix the issues instead. |
||
| 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<DiagnosticDescriptor> 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<INamedTypeSymbol> 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<INamedTypeSymbol> presentAttributes) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider moving these methods, in a generic form, into
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extracted generic attribute ordering methods into AttributeHelper as requested:
These methods are now reusable for future attribute-sorting analyzers. The DataRowOrderAnalyzer has been updated to use the new generic Commit: 9ed23b3 |
||
| { | ||
| return presentAttributes.Contains(_definitions.DataRowSymbol) && | ||
| presentAttributes.Any(attr => | ||
| attr.IsDerivedFrom(_definitions.TestMethodSymbol) || | ||
| attr.IsDerivedFrom(_definitions.DataTestMethodSymbol)); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<MethodDeclarationSyntax> | ||
| { | ||
| protected override string Title => "Move DataRow attributes above TestMethod"; | ||
|
|
||
| protected override DiagnosticId DiagnosticId => DiagnosticId.DataRowOrderInTestMethod; | ||
|
|
||
| protected override async Task<Document> ApplyFix(Document document, MethodDeclarationSyntax node, ImmutableDictionary<string, string> 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) | ||
|
Check warning on line 38 in Philips.CodeAnalysis.MsTestAnalyzers/DataRowOrderCodeFixProvider.cs
|
||
| { | ||
| var dataRowAttributes = new List<AttributeSyntax>(); | ||
| var testMethodAttributes = new List<AttributeSyntax>(); | ||
| var otherAttributes = new List<AttributeSyntax>(); | ||
|
|
||
| // 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<AttributeListSyntax> newAttributeLists = SyntaxFactory.List<AttributeListSyntax>(); | ||
|
|
||
| // 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); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems simpler:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored the HasAttributeAfterOther method using LINQ for improved readability. The new implementation uses SelectMany, Where, and other LINQ methods to eliminate nested loops and make the logic more declarative. Commit: 7d17055