Skip to content

Commit 3457435

Browse files
committed
Add analyzer for duplicate data row
1 parent 06c96c2 commit 3457435

20 files changed

+483
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
; Unshipped analyzer release
22
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
33

4+
### New Rules
5+
6+
Rule ID | Category | Severity | Notes
7+
--------|----------|----------|-------
8+
MSTEST0042 | Usage | Warning | DuplicateDataRowAnalyzer, [Documentation](https://learn.microsoft.com/dotnet/core/testing/mstest-analyzers/mstest0042)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System.Collections.Immutable;
5+
6+
using Analyzer.Utilities.Extensions;
7+
8+
using Microsoft.CodeAnalysis;
9+
using Microsoft.CodeAnalysis.Diagnostics;
10+
using Microsoft.Testing.Platform;
11+
12+
using MSTest.Analyzers.Helpers;
13+
14+
namespace MSTest.Analyzers;
15+
16+
/// <summary>
17+
/// MSTEST0042: <inheritdoc cref="Resources.DuplicateDataRowTitle"/>.
18+
/// </summary>
19+
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
20+
public sealed class DuplicateDataRowAnalyzer : DiagnosticAnalyzer
21+
{
22+
internal static readonly DiagnosticDescriptor Rule = DiagnosticDescriptorHelper.Create(
23+
DiagnosticIds.DuplicateDataRowRuleId,
24+
new LocalizableResourceString(nameof(Resources.DuplicateDataRowTitle), Resources.ResourceManager, typeof(Resources)),
25+
new LocalizableResourceString(nameof(Resources.DuplicateDataRowMessageFormat), Resources.ResourceManager, typeof(Resources)),
26+
null,
27+
Category.Usage,
28+
DiagnosticSeverity.Warning,
29+
isEnabledByDefault: true);
30+
31+
/// <inheritdoc />
32+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(Rule);
33+
34+
/// <inheritdoc />
35+
public override void Initialize(AnalysisContext context)
36+
{
37+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
38+
context.EnableConcurrentExecution();
39+
40+
context.RegisterCompilationStartAction(context =>
41+
{
42+
if (context.Compilation.TryGetOrCreateTypeByMetadataName(WellKnownTypeNames.MicrosoftVisualStudioTestToolsUnitTestingDataRowAttribute, out INamedTypeSymbol? dataRowAttribute))
43+
{
44+
context.RegisterSymbolAction(
45+
context => AnalyzeSymbol(context, dataRowAttribute),
46+
SymbolKind.Method);
47+
}
48+
});
49+
}
50+
51+
private static void AnalyzeSymbol(SymbolAnalysisContext context, INamedTypeSymbol dataRowAttribute)
52+
{
53+
var methodSymbol = (IMethodSymbol)context.Symbol;
54+
var dataRowArguments = new HashSet<ImmutableArray<TypedConstant>>(TypedConstantArrayComparer.Instance);
55+
56+
foreach (AttributeData attribute in methodSymbol.GetAttributes())
57+
{
58+
if (!dataRowAttribute.Equals(attribute.AttributeClass, SymbolEqualityComparer.Default))
59+
{
60+
continue;
61+
}
62+
63+
if (!dataRowArguments.Add(attribute.ConstructorArguments))
64+
{
65+
context.ReportDiagnostic((attribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation() ?? Location.None).CreateDiagnostic(Rule));
66+
}
67+
}
68+
}
69+
70+
private sealed class TypedConstantArrayComparer : IEqualityComparer<ImmutableArray<TypedConstant>>
71+
{
72+
public static TypedConstantArrayComparer Instance { get; } = new();
73+
74+
public bool Equals(ImmutableArray<TypedConstant> x, ImmutableArray<TypedConstant> y)
75+
{
76+
if (x.Length != y.Length)
77+
{
78+
return false;
79+
}
80+
81+
for (int i = 0; i < x.Length; i++)
82+
{
83+
if (AreTypedConstantEquals(x[i], y[i]))
84+
{
85+
return false;
86+
}
87+
}
88+
89+
return true;
90+
}
91+
92+
private static bool AreTypedConstantEquals(TypedConstant typedConstant1, TypedConstant typedConstant2)
93+
{
94+
// If the Kind doesn't match or the Type doesn't match, they are not equal.
95+
if (typedConstant1.Kind != typedConstant2.Kind ||
96+
SymbolEqualityComparer.Default.Equals(typedConstant1.Type, typedConstant2.Type))
97+
{
98+
return false;
99+
}
100+
101+
// If IsNull is true and the Kind is array, Values will return default(ImmutableArray<TypedConstant>), not empty.
102+
// To avoid dealing with that, we do the quick IsNull checks first.
103+
// If both are nulls, then we are good, everything is equal.
104+
if (typedConstant1.IsNull && typedConstant2.IsNull)
105+
{
106+
return true;
107+
}
108+
109+
// If only one is null, then we are not equal.
110+
if (typedConstant1.IsNull || typedConstant2.IsNull)
111+
{
112+
return false;
113+
}
114+
115+
// If the kind is array (at this point we know both have the same Kind), we compare Values.
116+
// Accessing `Value` property for arrays will throw so we need to have explicit check to decide whether
117+
// we compare `Value` or `Values`.
118+
if (typedConstant1.Kind == TypedConstantKind.Array)
119+
{
120+
return TypedConstantArrayComparer.Instance.Equals(typedConstant1.Values, typedConstant2.Values);
121+
}
122+
123+
// At this point, the type is matching and the kind is matching and is not array.
124+
return object.Equals(typedConstant1.Value, typedConstant2.Value);
125+
}
126+
127+
public int GetHashCode(ImmutableArray<TypedConstant> obj)
128+
{
129+
var hashCode = default(RoslynHashCode);
130+
foreach (TypedConstant typedConstant in obj)
131+
{
132+
hashCode.Add(typedConstant.Kind);
133+
hashCode.Add(SymbolEqualityComparer.Default.GetHashCode(typedConstant.Type));
134+
}
135+
136+
return hashCode.ToHashCode();
137+
}
138+
}
139+
}

src/Analyzers/MSTest.Analyzers/Helpers/DiagnosticIds.cs

+1
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,5 @@ internal static class DiagnosticIds
4646
public const string UseNewerAssertThrowsRuleId = "MSTEST0039";
4747
public const string AvoidUsingAssertsInAsyncVoidContextRuleId = "MSTEST0040";
4848
public const string UseConditionBaseWithTestClassRuleId = "MSTEST0041";
49+
public const string DuplicateDataRowRuleId = "MSTEST0042";
4950
}
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
#nullable enable
2+
MSTest.Analyzers.DuplicateDataRowAnalyzer
3+
MSTest.Analyzers.DuplicateDataRowAnalyzer.DuplicateDataRowAnalyzer() -> void
4+
override MSTest.Analyzers.DuplicateDataRowAnalyzer.Initialize(Microsoft.CodeAnalysis.Diagnostics.AnalysisContext! context) -> void
5+
override MSTest.Analyzers.DuplicateDataRowAnalyzer.SupportedDiagnostics.get -> System.Collections.Immutable.ImmutableArray<Microsoft.CodeAnalysis.DiagnosticDescriptor!>

src/Analyzers/MSTest.Analyzers/Resources.Designer.cs

+18
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Analyzers/MSTest.Analyzers/Resources.resx

+6
Original file line numberDiff line numberDiff line change
@@ -573,4 +573,10 @@ The type declaring these methods should also respect the following rules:
573573
<data name="UseConditionBaseWithTestClassMessageFormat" xml:space="preserve">
574574
<value>The attribute '{0}' which derives from 'ConditionBaseAttribute' should be used only on classes marked with `TestClassAttribute`</value>
575575
</data>
576+
<data name="DuplicateDataRowTitle" xml:space="preserve">
577+
<value>A duplicate 'DataRow' attribute is found</value>
578+
</data>
579+
<data name="DuplicateDataRowMessageFormat" xml:space="preserve">
580+
<value>Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</value>
581+
</data>
576582
</root>

src/Analyzers/MSTest.Analyzers/xlf/Resources.cs.xlf

+10
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,16 @@ Typ deklarující tyto metody by měl také respektovat následující pravidla:
351351
<target state="translated">System.ComponentModel.DescriptionAttribute nemá žádný vliv na testovací metody</target>
352352
<note />
353353
</trans-unit>
354+
<trans-unit id="DuplicateDataRowMessageFormat">
355+
<source>Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</source>
356+
<target state="new">Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</target>
357+
<note />
358+
</trans-unit>
359+
<trans-unit id="DuplicateDataRowTitle">
360+
<source>A duplicate 'DataRow' attribute is found</source>
361+
<target state="new">A duplicate 'DataRow' attribute is found</target>
362+
<note />
363+
</trans-unit>
354364
<trans-unit id="DynamicDataShouldBeValidDescription">
355365
<source>'DynamicData' entry should have the following layout to be valid:
356366
- should only be set on a test method;

src/Analyzers/MSTest.Analyzers/xlf/Resources.de.xlf

+10
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,16 @@ Der Typ, der diese Methoden deklariert, sollte auch die folgenden Regeln beachte
352352
<target state="translated">"System.ComponentModel.DescriptionAttribute" hat keine Auswirkungen auf Testmethoden.</target>
353353
<note />
354354
</trans-unit>
355+
<trans-unit id="DuplicateDataRowMessageFormat">
356+
<source>Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</source>
357+
<target state="new">Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</target>
358+
<note />
359+
</trans-unit>
360+
<trans-unit id="DuplicateDataRowTitle">
361+
<source>A duplicate 'DataRow' attribute is found</source>
362+
<target state="new">A duplicate 'DataRow' attribute is found</target>
363+
<note />
364+
</trans-unit>
355365
<trans-unit id="DynamicDataShouldBeValidDescription">
356366
<source>'DynamicData' entry should have the following layout to be valid:
357367
- should only be set on a test method;

src/Analyzers/MSTest.Analyzers/xlf/Resources.es.xlf

+10
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,16 @@ El tipo que declara estos métodos también debe respetar las reglas siguientes:
351351
<target state="translated">"System.ComponentModel.DescriptionAttribute" no tiene ningún efecto en los métodos de prueba</target>
352352
<note />
353353
</trans-unit>
354+
<trans-unit id="DuplicateDataRowMessageFormat">
355+
<source>Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</source>
356+
<target state="new">Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</target>
357+
<note />
358+
</trans-unit>
359+
<trans-unit id="DuplicateDataRowTitle">
360+
<source>A duplicate 'DataRow' attribute is found</source>
361+
<target state="new">A duplicate 'DataRow' attribute is found</target>
362+
<note />
363+
</trans-unit>
354364
<trans-unit id="DynamicDataShouldBeValidDescription">
355365
<source>'DynamicData' entry should have the following layout to be valid:
356366
- should only be set on a test method;

src/Analyzers/MSTest.Analyzers/xlf/Resources.fr.xlf

+10
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,16 @@ Le type doit être une classe
351351
<target state="translated">« System.ComponentModel.DescriptionAttribute » n’a aucun effet sur les méthodes de test</target>
352352
<note />
353353
</trans-unit>
354+
<trans-unit id="DuplicateDataRowMessageFormat">
355+
<source>Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</source>
356+
<target state="new">Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</target>
357+
<note />
358+
</trans-unit>
359+
<trans-unit id="DuplicateDataRowTitle">
360+
<source>A duplicate 'DataRow' attribute is found</source>
361+
<target state="new">A duplicate 'DataRow' attribute is found</target>
362+
<note />
363+
</trans-unit>
354364
<trans-unit id="DynamicDataShouldBeValidDescription">
355365
<source>'DynamicData' entry should have the following layout to be valid:
356366
- should only be set on a test method;

src/Analyzers/MSTest.Analyzers/xlf/Resources.it.xlf

+10
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,16 @@ Anche il tipo che dichiara questi metodi deve rispettare le regole seguenti:
351351
<target state="translated">'System.ComponentModel.DescriptionAttribute' non ha alcun effetto sui metodi di test.</target>
352352
<note />
353353
</trans-unit>
354+
<trans-unit id="DuplicateDataRowMessageFormat">
355+
<source>Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</source>
356+
<target state="new">Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</target>
357+
<note />
358+
</trans-unit>
359+
<trans-unit id="DuplicateDataRowTitle">
360+
<source>A duplicate 'DataRow' attribute is found</source>
361+
<target state="new">A duplicate 'DataRow' attribute is found</target>
362+
<note />
363+
</trans-unit>
354364
<trans-unit id="DynamicDataShouldBeValidDescription">
355365
<source>'DynamicData' entry should have the following layout to be valid:
356366
- should only be set on a test method;

src/Analyzers/MSTest.Analyzers/xlf/Resources.ja.xlf

+10
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,16 @@ The type declaring these methods should also respect the following rules:
351351
<target state="translated">'System.ComponentModel.DescriptionAttribute' はテスト メソッドに影響しません</target>
352352
<note />
353353
</trans-unit>
354+
<trans-unit id="DuplicateDataRowMessageFormat">
355+
<source>Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</source>
356+
<target state="new">Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</target>
357+
<note />
358+
</trans-unit>
359+
<trans-unit id="DuplicateDataRowTitle">
360+
<source>A duplicate 'DataRow' attribute is found</source>
361+
<target state="new">A duplicate 'DataRow' attribute is found</target>
362+
<note />
363+
</trans-unit>
354364
<trans-unit id="DynamicDataShouldBeValidDescription">
355365
<source>'DynamicData' entry should have the following layout to be valid:
356366
- should only be set on a test method;

src/Analyzers/MSTest.Analyzers/xlf/Resources.ko.xlf

+10
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,16 @@ The type declaring these methods should also respect the following rules:
351351
<target state="translated">'System.ComponentModel.DescriptionAttribute'는 테스트 메서드에 영향을 주지 않습니다.</target>
352352
<note />
353353
</trans-unit>
354+
<trans-unit id="DuplicateDataRowMessageFormat">
355+
<source>Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</source>
356+
<target state="new">Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</target>
357+
<note />
358+
</trans-unit>
359+
<trans-unit id="DuplicateDataRowTitle">
360+
<source>A duplicate 'DataRow' attribute is found</source>
361+
<target state="new">A duplicate 'DataRow' attribute is found</target>
362+
<note />
363+
</trans-unit>
354364
<trans-unit id="DynamicDataShouldBeValidDescription">
355365
<source>'DynamicData' entry should have the following layout to be valid:
356366
- should only be set on a test method;

src/Analyzers/MSTest.Analyzers/xlf/Resources.pl.xlf

+10
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,16 @@ Typ deklarujący te metody powinien również przestrzegać następujących regu
351351
<target state="translated">Element „System.ComponentModel.DescriptionAttribute” nie ma wpływu na metody testowe</target>
352352
<note />
353353
</trans-unit>
354+
<trans-unit id="DuplicateDataRowMessageFormat">
355+
<source>Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</source>
356+
<target state="new">Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</target>
357+
<note />
358+
</trans-unit>
359+
<trans-unit id="DuplicateDataRowTitle">
360+
<source>A duplicate 'DataRow' attribute is found</source>
361+
<target state="new">A duplicate 'DataRow' attribute is found</target>
362+
<note />
363+
</trans-unit>
354364
<trans-unit id="DynamicDataShouldBeValidDescription">
355365
<source>'DynamicData' entry should have the following layout to be valid:
356366
- should only be set on a test method;

src/Analyzers/MSTest.Analyzers/xlf/Resources.pt-BR.xlf

+10
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,16 @@ O tipo que declara esses métodos também deve respeitar as seguintes regras:
351351
<target state="translated">'System.ComponentModel.DescriptionAttribute' não tem efeito sobre métodos de teste</target>
352352
<note />
353353
</trans-unit>
354+
<trans-unit id="DuplicateDataRowMessageFormat">
355+
<source>Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</source>
356+
<target state="new">Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</target>
357+
<note />
358+
</trans-unit>
359+
<trans-unit id="DuplicateDataRowTitle">
360+
<source>A duplicate 'DataRow' attribute is found</source>
361+
<target state="new">A duplicate 'DataRow' attribute is found</target>
362+
<note />
363+
</trans-unit>
354364
<trans-unit id="DynamicDataShouldBeValidDescription">
355365
<source>'DynamicData' entry should have the following layout to be valid:
356366
- should only be set on a test method;

src/Analyzers/MSTest.Analyzers/xlf/Resources.ru.xlf

+10
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,16 @@ The type declaring these methods should also respect the following rules:
357357
<target state="translated">"System.ComponentModel.DescriptionAttribute" не действует на методы тестирования</target>
358358
<note />
359359
</trans-unit>
360+
<trans-unit id="DuplicateDataRowMessageFormat">
361+
<source>Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</source>
362+
<target state="new">Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</target>
363+
<note />
364+
</trans-unit>
365+
<trans-unit id="DuplicateDataRowTitle">
366+
<source>A duplicate 'DataRow' attribute is found</source>
367+
<target state="new">A duplicate 'DataRow' attribute is found</target>
368+
<note />
369+
</trans-unit>
360370
<trans-unit id="DynamicDataShouldBeValidDescription">
361371
<source>'DynamicData' entry should have the following layout to be valid:
362372
- should only be set on a test method;

src/Analyzers/MSTest.Analyzers/xlf/Resources.tr.xlf

+10
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,16 @@ Bu yöntemleri bildiren tipin ayrıca aşağıdaki kurallara uyması gerekir:
351351
<target state="translated">'System.ComponentModel.DescriptionAttribute' test yöntemlerini etkilemez</target>
352352
<note />
353353
</trans-unit>
354+
<trans-unit id="DuplicateDataRowMessageFormat">
355+
<source>Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</source>
356+
<target state="new">Do not duplicate 'DataRow' attributes. This is usually a copy/paste error.</target>
357+
<note />
358+
</trans-unit>
359+
<trans-unit id="DuplicateDataRowTitle">
360+
<source>A duplicate 'DataRow' attribute is found</source>
361+
<target state="new">A duplicate 'DataRow' attribute is found</target>
362+
<note />
363+
</trans-unit>
354364
<trans-unit id="DynamicDataShouldBeValidDescription">
355365
<source>'DynamicData' entry should have the following layout to be valid:
356366
- should only be set on a test method;

0 commit comments

Comments
 (0)