-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathTestShouldUseDisplayNameAnalyzer.cs
More file actions
143 lines (122 loc) · 5.15 KB
/
Copy pathTestShouldUseDisplayNameAnalyzer.cs
File metadata and controls
143 lines (122 loc) · 5.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
// © 2025 Koninklijke Philips N.V. See License.md in the project root for license information.
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: true, 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
foreach (AttributeListSyntax attributeList in methodDeclaration.AttributeLists)
{
foreach (AttributeSyntax attribute in attributeList.Attributes)
{
if (Helper.ForAttributes.IsDataRowAttribute(attribute, context))
{
// Check if this DataRow has a comment but no DisplayName
var hasDisplayName = attribute.ArgumentList?.Arguments.Any(arg =>
arg.NameEquals?.Name.Identifier.ValueText == "DisplayName") == true;
if (!hasDisplayName)
{
// Look for trailing comment on the same line
var comment = GetTrailingComment(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 = GetLeadingComment(methodDeclaration);
if (!string.IsNullOrWhiteSpace(comment))
{
var diagnostic = Diagnostic.Create(Rule, methodDeclaration.Identifier.GetLocation());
context.ReportDiagnostic(diagnostic);
}
}
}
private string GetTrailingComment(AttributeSyntax attribute)
{
SyntaxToken token = attribute.GetLastToken();
SyntaxTrivia trivia = token.TrailingTrivia.FirstOrDefault(t => t.IsKind(SyntaxKind.SingleLineCommentTrivia));
if (trivia.IsKind(SyntaxKind.SingleLineCommentTrivia))
{
return ExtractCommentText(trivia.ToString());
}
return string.Empty;
}
private string GetLeadingComment(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 text;
}
return string.Empty;
}
}
}
}