-
Notifications
You must be signed in to change notification settings - Fork 14
feat: Add MsTest analyzer to suggest DisplayName and Description attributes for commented tests #841
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?
feat: Add MsTest analyzer to suggest DisplayName and Description attributes for commented tests #841
Changes from all commits
8c6eaa8
8ebe896
e87072b
1b0af01
809ae19
fac9da7
025dae2
c8ace77
5ad28b3
2113104
d900363
a7cb058
e88eb83
47725f9
f3d313f
4288bf9
eaed4cd
57956d8
3fdc09d
cb7d7a6
7022935
c256631
d818605
a6c4257
cbb1f61
b900f87
0962cb6
bf46025
360bd05
799f0f7
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,68 @@ | ||
| # PH2150: Test should use DisplayName or Description attribute instead of comments | ||
|
|
||
| | Property | Value | | ||
| |--|--| | ||
| | Package | [Philips.CodeAnalysis.MsTestAnalyzers](https://www.nuget.org/packages/Philips.CodeAnalysis.MsTestAnalyzers) | | ||
| | Diagnostic ID | PH2150 | | ||
| | Category | [MsTest](../MsTest.md) | | ||
| | Analyzer | [TestShouldUseDisplayNameAnalyzer](https://github.com/philips-software/roslyn-analyzers/blob/main/Philips.CodeAnalysis.MsTestAnalyzers/TestShouldUseDisplayNameAnalyzer.cs) | ||
| | CodeFix | No | | ||
| | Severity | Info | | ||
| | Enabled By Default | No | | ||
|
|
||
| ## Introduction | ||
|
|
||
| Test methods and DataRow attributes often contain inline comments that describe their purpose, but these comments are not visible in test runners. MSTest provides better alternatives: `Description` attribute for test methods and `DisplayName` parameter for DataRow attributes. | ||
|
|
||
| ## How to solve | ||
|
|
||
| For test methods with meaningful comments, add a `Description` attribute instead of using inline comments. | ||
|
|
||
| For DataRow attributes with meaningful comments, add a `DisplayName` parameter instead of using inline comments. | ||
|
|
||
| ## Example | ||
|
|
||
| Code that triggers a diagnostic: | ||
| ``` cs | ||
| [TestClass] | ||
| public class TestClass | ||
| { | ||
| // This test verifies that addition works correctly | ||
| [TestMethod] | ||
| public void TestAddition() { } | ||
|
|
||
| [DataTestMethod] | ||
| [DataRow(1, 2)] // Should add positive numbers | ||
| [DataRow(-1, 1)] // Should handle negative numbers | ||
| public void TestAdditionWithData(int a, int b) { } | ||
| } | ||
| ``` | ||
|
|
||
| And the replacement code: | ||
| ``` cs | ||
| [TestClass] | ||
| public class TestClass | ||
| { | ||
| [TestMethod] | ||
| [Description("This test verifies that addition works correctly")] | ||
| public void TestAddition() { } | ||
|
|
||
| [DataTestMethod] | ||
| [DataRow(1, 2, DisplayName = "Should add positive numbers")] | ||
| [DataRow(-1, 1, DisplayName = "Should handle negative numbers")] | ||
| public void TestAdditionWithData(int a, int b) { } | ||
| } | ||
| ``` | ||
|
|
||
| ## Configuration | ||
|
|
||
| This analyzer ignores certain boilerplate comment patterns to reduce noise: | ||
| - Comments starting with `TODO` | ||
| - Comments starting with `FIXME` | ||
| - Comments starting with `HACK` | ||
| - Comments starting with `NOTE` | ||
| - Comments starting with `BUG` | ||
|
|
||
| Additionally, comments with 5 or fewer characters are considered too short to be meaningful and are ignored. | ||
|
|
||
| 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 |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| // © 2025 Koninklijke Philips N.V. See License.md in the project root for license information. | ||
| // © 2025 Koninklijke Philips N.V. See License.md in the project root for license information. | ||
|
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 reverting, as this is out of scope and in another PR already |
||
|
|
||
| using Microsoft.CodeAnalysis; | ||
| using Microsoft.CodeAnalysis.CSharp; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| // © 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; | ||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
| using Microsoft.CodeAnalysis.Diagnostics; | ||
| using Philips.CodeAnalysis.Common; | ||
|
|
||
| namespace Philips.CodeAnalysis.MsTestAnalyzers | ||
| { | ||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
| public class TestShouldUseDisplayNameAnalyzer : TestMethodDiagnosticAnalyzer | ||
| { | ||
| private const string Title = @"Test should use DisplayName or Description attribute instead of comments"; | ||
| public const string MessageFormat = @"Consider using DisplayName parameter for DataRow or Description attribute for test method instead of inline comments"; | ||
| private const string Description = @"Using DisplayName parameter for DataRow attributes or Description attribute for test methods makes test purpose more visible in test runners and provides better documentation."; | ||
| private const string Category = Categories.MsTest; | ||
|
|
||
| private static readonly DiagnosticDescriptor Rule = new(DiagnosticId.UseDisplayNameOrDescription.ToId(), Title, MessageFormat, Category, DiagnosticSeverity.Error, isEnabledByDefault: false, description: Description); | ||
|
|
||
| public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule); | ||
|
|
||
| protected override TestMethodImplementation OnInitializeTestMethodAnalyzer(AnalyzerOptions options, Compilation compilation, MsTestAttributeDefinitions definitions) | ||
| { | ||
| return new TestShouldUseDisplayName(definitions, Helper); | ||
| } | ||
|
|
||
| public class TestShouldUseDisplayName : TestMethodImplementation | ||
| { | ||
| public TestShouldUseDisplayName(MsTestAttributeDefinitions definitions, Helper helper) : base(definitions, helper) | ||
| { } | ||
|
|
||
| protected override void OnTestMethod(SyntaxNodeAnalysisContext context, MethodDeclarationSyntax methodDeclaration, IMethodSymbol methodSymbol, bool isDataTestMethod) | ||
| { | ||
| if (isDataTestMethod) | ||
| { | ||
| CheckDataTestMethodForDisplayName(context, methodDeclaration); | ||
| } | ||
| else | ||
| { | ||
| CheckTestMethodForDescription(context, methodDeclaration); | ||
| } | ||
| } | ||
|
|
||
| private void CheckDataTestMethodForDisplayName(SyntaxNodeAnalysisContext context, MethodDeclarationSyntax methodDeclaration) | ||
| { | ||
| // Check for DataRow attributes with comments but no DisplayName | ||
| IEnumerable<AttributeSyntax> dataRowAttributes = methodDeclaration.AttributeLists | ||
| .SelectMany(list => list.Attributes) | ||
| .Where(attr => Helper.ForAttributes.IsDataRowAttribute(attr, context)); | ||
|
|
||
| foreach (AttributeSyntax attribute in dataRowAttributes) | ||
| { | ||
| // Check if this DataRow has a comment but no DisplayName | ||
| var hasDisplayName = attribute.ArgumentList?.Arguments.Any(arg => | ||
| arg.NameEquals?.Name.Identifier.ValueText == "DisplayName") ?? false; | ||
|
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. You don't need the |
||
|
|
||
| if (!hasDisplayName) | ||
| { | ||
| // Look for trailing comment on the same line | ||
| var comment = GetMeaningfulTrailingComment(attribute); | ||
| if (!string.IsNullOrWhiteSpace(comment)) | ||
| { | ||
| var diagnostic = Diagnostic.Create(Rule, attribute.GetLocation()); | ||
| context.ReportDiagnostic(diagnostic); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private void CheckTestMethodForDescription(SyntaxNodeAnalysisContext context, MethodDeclarationSyntax methodDeclaration) | ||
| { | ||
| // Check if test method has Description attribute | ||
| var hasDescription = Helper.ForAttributes.HasAttribute(methodDeclaration.AttributeLists, context, MsTestFrameworkDefinitions.DescriptionAttribute, out _, out _); | ||
|
|
||
| if (!hasDescription) | ||
| { | ||
| // Look for leading comment before the method | ||
| var comment = GetMeaningfulLeadingComment(methodDeclaration); | ||
| if (!string.IsNullOrWhiteSpace(comment)) | ||
| { | ||
| var diagnostic = Diagnostic.Create(Rule, methodDeclaration.Identifier.GetLocation()); | ||
| context.ReportDiagnostic(diagnostic); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private string GetMeaningfulTrailingComment(AttributeSyntax attribute) | ||
| { | ||
| // Look for the parent AttributeListSyntax to get the closing bracket | ||
| if (attribute.Parent is AttributeListSyntax attributeList) | ||
| { | ||
| SyntaxToken closeBracketToken = attributeList.CloseBracketToken; | ||
| SyntaxTrivia trivia = closeBracketToken.TrailingTrivia.FirstOrDefault(t => t.IsKind(SyntaxKind.SingleLineCommentTrivia)); | ||
|
|
||
| if (trivia.IsKind(SyntaxKind.SingleLineCommentTrivia)) | ||
| { | ||
| return ExtractCommentText(trivia.ToString()); | ||
| } | ||
| } | ||
|
|
||
| return string.Empty; | ||
| } | ||
|
|
||
| private string GetMeaningfulLeadingComment(MethodDeclarationSyntax methodDeclaration) | ||
| { | ||
| SyntaxTriviaList leadingTrivia = methodDeclaration.GetLeadingTrivia(); | ||
|
|
||
| // Look for single-line comment immediately before the method | ||
| SyntaxTrivia comment = leadingTrivia.LastOrDefault(t => t.IsKind(SyntaxKind.SingleLineCommentTrivia)); | ||
| if (comment.IsKind(SyntaxKind.SingleLineCommentTrivia)) | ||
| { | ||
| return ExtractCommentText(comment.ToString()); | ||
| } | ||
|
|
||
| return string.Empty; | ||
| } | ||
|
|
||
| private string ExtractCommentText(string commentTrivia) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(commentTrivia)) | ||
| { | ||
| return string.Empty; | ||
| } | ||
|
|
||
| // Remove // and trim whitespace | ||
| var text = commentTrivia.Trim(); | ||
| if (text.StartsWith("//")) | ||
| { | ||
| text = text.Substring(2).Trim(); | ||
| } | ||
|
|
||
| // Only consider meaningful comments (more than just a few characters) | ||
| if (text.Length <= 5) | ||
| { | ||
| return string.Empty; | ||
| } | ||
|
|
||
| // Filter out boilerplate comment patterns | ||
| var upperText = text.ToUpperInvariant(); | ||
|
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. Add these boilerplate exceptions to the documentation in PH2150.md
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. Added documentation for the boilerplate comment patterns that are filtered out (TODO, FIXME, HACK, NOTE, BUG) in the Configuration section of PH2150.md. Also documented the 5-character minimum length requirement. Commit cbb1f61. |
||
| if (upperText.StartsWith("TODO") || | ||
| upperText.StartsWith("FIXME") || | ||
| upperText.StartsWith("HACK") || | ||
| upperText.StartsWith("NOTE") || | ||
| upperText.StartsWith("BUG")) | ||
| { | ||
| return string.Empty; | ||
| } | ||
|
|
||
| return text; | ||
| } | ||
| } | ||
| } | ||
| } | ||
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.
I prefer DisplayName to be very short, almost steno. So in this case, I would go for
Positive numbersandNegative numbers