Skip to content

Commit 357178d

Browse files
authored
Analyzer - theory data row inside theory data (#195)
1 parent 965a11c commit 357178d

File tree

5 files changed

+342
-1
lines changed

5 files changed

+342
-1
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using System.Composition;
2+
using System.Linq;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using Microsoft.CodeAnalysis;
6+
using Microsoft.CodeAnalysis.CodeActions;
7+
using Microsoft.CodeAnalysis.CodeFixes;
8+
using Microsoft.CodeAnalysis.CSharp;
9+
using Microsoft.CodeAnalysis.CSharp.Syntax;
10+
using Microsoft.CodeAnalysis.Editing;
11+
12+
namespace Xunit.Analyzers.Fixes;
13+
14+
[ExportCodeFixProvider(LanguageNames.CSharp), Shared]
15+
public class TheoryDataShouldNotUseTheoryDataRowFixer() :
16+
XunitCodeFixProvider(Descriptors.X1052_TheoryDataShouldNotUseITheoryDataRow.Id)
17+
{
18+
public const string Key_UseIEnumerable = "xUnit1052_UseIEnumerable";
19+
20+
public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context)
21+
{
22+
var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
23+
if (root is null)
24+
return;
25+
26+
foreach (var diagnostic in context.Diagnostics)
27+
{
28+
var span = diagnostic.Location.SourceSpan;
29+
var node = root.FindNode(span);
30+
31+
if (node is not GenericNameSyntax genericNameNode)
32+
return;
33+
34+
if (genericNameNode.TypeArgumentList.Arguments.Count != 1)
35+
return;
36+
37+
if (!IsPartOfOnlyTypeDeclaration(genericNameNode))
38+
return;
39+
40+
context.RegisterCodeFix(
41+
CodeAction.Create(
42+
"Use IEnumerable instead of TheoryData",
43+
ct => ConvertToIEnumerable(context.Document, genericNameNode, ct),
44+
Key_UseIEnumerable
45+
),
46+
diagnostic
47+
);
48+
}
49+
}
50+
51+
static bool IsPartOfOnlyTypeDeclaration(GenericNameSyntax genericName)
52+
{
53+
var parent = genericName.Parent;
54+
55+
if (parent is VariableDeclarationSyntax variableDeclaration)
56+
return variableDeclaration.Variables.All(v => v.Initializer is null);
57+
58+
if (parent is PropertyDeclarationSyntax propertyDeclaration)
59+
return propertyDeclaration.Initializer is null;
60+
61+
return parent is ParameterSyntax or MethodDeclarationSyntax;
62+
}
63+
64+
static async Task<Document> ConvertToIEnumerable(
65+
Document document,
66+
GenericNameSyntax node,
67+
CancellationToken ct)
68+
{
69+
var editor = await DocumentEditor.CreateAsync(document, ct).ConfigureAwait(false);
70+
var token = SyntaxFactory.IdentifierName("IEnumerable").Identifier;
71+
var newGenericName = SyntaxFactory.GenericName(token, node.TypeArgumentList);
72+
73+
editor.ReplaceNode(node, newGenericName);
74+
75+
return editor.GetChangedDocument();
76+
}
77+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using System.Threading.Tasks;
2+
using Microsoft.CodeAnalysis.CSharp;
3+
using Xunit;
4+
using Verify = CSharpVerifier<Xunit.Analyzers.TheoryDataShouldNotUseTheoryDataRow>;
5+
6+
public class TheoryDataShouldNotUseTheoryDataRowTests
7+
{
8+
[Fact]
9+
public async Task AcceptanceTest()
10+
{
11+
var source = /* lang=C#-test */ """
12+
using System;
13+
using System.Collections.Generic;
14+
using Xunit;
15+
16+
public class Triggers1 {
17+
// Constructors
18+
public Triggers1([|TheoryData<ITheoryDataRow>|] data) { }
19+
public Triggers1([|TheoryData<TheoryDataRow<int>>|] data) { }
20+
public Triggers1([|TheoryData<MyRow>|] data) { }
21+
22+
// Fields
23+
private [|TheoryData<ITheoryDataRow>|] field1 = new [|TheoryData<ITheoryDataRow>|]();
24+
private [|TheoryData<TheoryDataRow<int>>|] field2 = new [|TheoryData<TheoryDataRow<int>>|]();
25+
private [|TheoryData<MyRow>|] field3 = new [|TheoryData<MyRow>|]();
26+
27+
// Array fields
28+
public [|TheoryData<ITheoryDataRow>|][] field11 = null;
29+
public [|TheoryData<TheoryDataRow<int>>|][] field12 = null;
30+
public [|TheoryData<MyRow>|][] field13 = null;
31+
32+
// Static fields
33+
private static [|TheoryData<ITheoryDataRow>|] field21 = null;
34+
private static [|TheoryData<TheoryDataRow<int>>|] field22 = null;
35+
private static [|TheoryData<MyRow>|] field23 = null;
36+
37+
// Properties
38+
public [|TheoryData<ITheoryDataRow>|] property1 { get; set; } = new [|TheoryData<ITheoryDataRow>|]();
39+
public [|TheoryData<TheoryDataRow<int>>|] property2 { get; set; } = new [|TheoryData<TheoryDataRow<int>>|]();
40+
public [|TheoryData<MyRow>|] property3 { get; set; } = new [|TheoryData<MyRow>|]();
41+
42+
// Methods
43+
public [|TheoryData<ITheoryDataRow>|] Method1() { return null; }
44+
public [|TheoryData<TheoryDataRow<int>>|] Method2() { return null; }
45+
public [|TheoryData<MyRow>|] Method3() { return null; }
46+
}
47+
48+
// Generic constraints
49+
class Triggers2<T> where T : ITheoryDataRow {
50+
[|TheoryData<T>|] data = null;
51+
}
52+
53+
class Triggers3<T> where T : MyRow {
54+
[|TheoryData<T>|] data = null;
55+
}
56+
57+
public class DoesNotTrigger {
58+
IEnumerable<ITheoryDataRow> field1 = new List<ITheoryDataRow>();
59+
IEnumerable<TheoryDataRow<int>> field2 = new List<TheoryDataRow<int>>();
60+
IEnumerable<MyRow> field3 = new List<MyRow>();
61+
62+
IEnumerable<ITheoryDataRow> property1 { get; set; } = new List<ITheoryDataRow>();
63+
IEnumerable<TheoryDataRow<int>> property2 { get; set; } = new List<TheoryDataRow<int>>();
64+
IEnumerable<MyRow> property3 { get; set; } = new List<MyRow>();
65+
66+
IEnumerable<ITheoryDataRow> method1() { return null; }
67+
IEnumerable<TheoryDataRow<int>> method2() { return null; }
68+
IEnumerable<MyRow> method3() { return null; }
69+
}
70+
71+
public class MyRow : ITheoryDataRow {
72+
public object?[] GetData() { return null; }
73+
public bool? Explicit { get; }
74+
public string? Label { get; }
75+
public string? Skip { get; }
76+
public Type? SkipType { get; }
77+
public string? SkipUnless { get; }
78+
public string? SkipWhen { get; }
79+
public string? TestDisplayName { get; }
80+
public int? Timeout { get; }
81+
public Dictionary<string, HashSet<string>>? Traits { get; }
82+
}
83+
""";
84+
85+
await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp8, source);
86+
}
87+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using System.Threading.Tasks;
2+
using Microsoft.CodeAnalysis.CSharp;
3+
using Xunit;
4+
using Xunit.Analyzers.Fixes;
5+
using Verify = CSharpVerifier<Xunit.Analyzers.TheoryDataShouldNotUseTheoryDataRow>;
6+
7+
public class TheoryDataShouldNotUseTheoryDataRowFixerTests
8+
{
9+
const string myRowSource = /* lang=c#-test */ """
10+
public class MyRow : ITheoryDataRow {
11+
public object?[] GetData() { return null; }
12+
public bool? Explicit { get; }
13+
public string? Label { get; }
14+
public string? Skip { get; }
15+
public Type? SkipType { get; }
16+
public string? SkipUnless { get; }
17+
public string? SkipWhen { get; }
18+
public string? TestDisplayName { get; }
19+
public int? Timeout { get; }
20+
public Dictionary<string, HashSet<string>>? Traits { get; }
21+
}
22+
""";
23+
24+
[Fact]
25+
public async Task AcceptanceTest_Fixable()
26+
{
27+
var before = /* lang=c#-test */ """
28+
using System;
29+
using System.Collections.Generic;
30+
using Xunit;
31+
32+
public class TestClass {
33+
private [|TheoryData<ITheoryDataRow>|] field1;
34+
private [|TheoryData<TheoryDataRow<int>>|] field2;
35+
private [|TheoryData<MyRow>|] field3;
36+
37+
public [|TheoryData<ITheoryDataRow>|] property1 { get; set; }
38+
public [|TheoryData<TheoryDataRow<int>>|] property2 { get; set; }
39+
public [|TheoryData<MyRow>|] property3 { get; set; }
40+
41+
public [|TheoryData<ITheoryDataRow>|] method1() { [|TheoryData<ITheoryDataRow>|] data; return null; }
42+
public [|TheoryData<TheoryDataRow<int>>|] method2() { [|TheoryData<TheoryDataRow<int>>|] data; return null; }
43+
public [|TheoryData<MyRow>|] method3() { [|TheoryData<MyRow>|] data; return null; }
44+
}
45+
""" + myRowSource;
46+
var after = /* lang=c#-test */ """
47+
using System;
48+
using System.Collections.Generic;
49+
using Xunit;
50+
51+
public class TestClass {
52+
private IEnumerable<ITheoryDataRow> field1;
53+
private IEnumerable<TheoryDataRow<int>> field2;
54+
private IEnumerable<MyRow> field3;
55+
56+
public IEnumerable<ITheoryDataRow> property1 { get; set; }
57+
public IEnumerable<TheoryDataRow<int>> property2 { get; set; }
58+
public IEnumerable<MyRow> property3 { get; set; }
59+
60+
public IEnumerable<ITheoryDataRow> method1() { IEnumerable<ITheoryDataRow> data; return null; }
61+
public IEnumerable<TheoryDataRow<int>> method2() { IEnumerable<TheoryDataRow<int>> data; return null; }
62+
public IEnumerable<MyRow> method3() { IEnumerable<MyRow> data; return null; }
63+
}
64+
""" + myRowSource;
65+
66+
await Verify.VerifyCodeFixV3(LanguageVersion.CSharp9, before, after, TheoryDataShouldNotUseTheoryDataRowFixer.Key_UseIEnumerable);
67+
}
68+
69+
[Fact]
70+
public async Task AcceptanceTest_Unfixable()
71+
{
72+
var before = /* lang=c#-test */ """
73+
using System;
74+
using System.Collections.Generic;
75+
using Xunit;
76+
77+
public class TestClass {
78+
private [|TheoryData<ITheoryDataRow>|] field11 = new();
79+
private [|TheoryData<TheoryDataRow<int>>|] field12 = new();
80+
private [|TheoryData<MyRow>|] field13 = new();
81+
82+
private [|TheoryData<ITheoryDataRow, int>|] field21;
83+
private [|TheoryData<TheoryDataRow<int>, int>|] field22;
84+
private [|TheoryData<MyRow, int>|] field23;
85+
86+
public [|TheoryData<ITheoryDataRow>|] property11 { get; set; } = new();
87+
public [|TheoryData<TheoryDataRow<int>>|] property12 { get; set; } = new();
88+
public [|TheoryData<MyRow>|] property13 { get; set; } = new();
89+
90+
public [|TheoryData<ITheoryDataRow, int>|] property21 { get; set; }
91+
public [|TheoryData<TheoryDataRow<int>, int>|] property22 { get; set; }
92+
public [|TheoryData<MyRow, int>|] property23 { get; set; }
93+
94+
public [|TheoryData<ITheoryDataRow, int>|] method11() { [|TheoryData<ITheoryDataRow, int>|] data; return null; }
95+
public [|TheoryData<TheoryDataRow<int>, int>|] method12() { [|TheoryData<TheoryDataRow<int>, int>|] data; return null; }
96+
public [|TheoryData<MyRow, int>|] method13() { [|TheoryData<MyRow, int>|] data; return null; }
97+
}
98+
""" + myRowSource;
99+
100+
await Verify.VerifyCodeFixV3(LanguageVersion.CSharp9, before, after: before, TheoryDataShouldNotUseTheoryDataRowFixer.Key_UseIEnumerable);
101+
}
102+
}

src/xunit.analyzers/Utility/Descriptors.xUnit1xxx.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,14 @@ public static partial class Descriptors
474474
"Calls to methods which accept CancellationToken should use TestContext.Current.CancellationToken to allow test cancellation to be more responsive."
475475
);
476476

477-
// Placeholder for rule X1052
477+
public static DiagnosticDescriptor X1052_TheoryDataShouldNotUseITheoryDataRow { get; } =
478+
Diagnostic(
479+
"xUnit1052",
480+
"Avoid using 'TheoryData<...>' with types that implement 'ITheoryDataRow'.",
481+
Usage,
482+
Warning,
483+
"'TheoryData<...>' should not be used with one or more type arguments that implement 'ITheoryDataRow' or a derived variant. This usage is not supported. Use either 'TheoryData' or a type of 'ITheoryDataRow' exclusively."
484+
);
478485

479486
// Placeholder for rule X1053
480487

@@ -489,4 +496,6 @@ public static partial class Descriptors
489496
// Placeholder for rule X1058
490497

491498
// Placeholder for rule X1059
499+
500+
// Placeholder for rule X1060
492501
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System.Linq;
2+
using Microsoft.CodeAnalysis;
3+
using Microsoft.CodeAnalysis.CSharp;
4+
using Microsoft.CodeAnalysis.CSharp.Syntax;
5+
using Microsoft.CodeAnalysis.Diagnostics;
6+
7+
namespace Xunit.Analyzers;
8+
9+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
10+
public class TheoryDataShouldNotUseTheoryDataRow() :
11+
XunitV3DiagnosticAnalyzer(Descriptors.X1052_TheoryDataShouldNotUseITheoryDataRow)
12+
{
13+
public override void AnalyzeCompilation(
14+
CompilationStartAnalysisContext context,
15+
XunitContext xunitContext)
16+
{
17+
Guard.ArgumentNotNull(context);
18+
Guard.ArgumentNotNull(xunitContext);
19+
20+
var iTheoryDataRowSymbol = TypeSymbolFactory.ITheoryDataRow_V3(context.Compilation);
21+
if (iTheoryDataRowSymbol is null)
22+
return;
23+
24+
var theoryDataTypes = TypeSymbolFactory.TheoryData_ByGenericArgumentCount(context.Compilation);
25+
26+
context.RegisterSyntaxNodeAction(context =>
27+
{
28+
var genericName = (GenericNameSyntax)context.Node;
29+
30+
if (context.SemanticModel.GetSymbolInfo(genericName).Symbol is not INamedTypeSymbol typeSymbol)
31+
return;
32+
33+
if (!theoryDataTypes.TryGetValue(typeSymbol.TypeArguments.Length, out var expectedSymbol))
34+
return;
35+
36+
if (!SymbolEqualityComparer.Default.Equals(expectedSymbol, typeSymbol.OriginalDefinition))
37+
return;
38+
39+
foreach (var typeArg in typeSymbol.TypeArguments)
40+
if (IsOrImplementsITheoryDataRow(typeArg, iTheoryDataRowSymbol))
41+
context.ReportDiagnostic(
42+
Diagnostic.Create(
43+
Descriptors.X1052_TheoryDataShouldNotUseITheoryDataRow,
44+
genericName.GetLocation()
45+
)
46+
);
47+
}, SyntaxKind.GenericName);
48+
}
49+
50+
static bool IsOrImplementsITheoryDataRow(
51+
ITypeSymbol typeArg,
52+
INamedTypeSymbol iTheoryDataSymbol)
53+
{
54+
if (SymbolEqualityComparer.Default.Equals(typeArg, iTheoryDataSymbol) ||
55+
typeArg.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, iTheoryDataSymbol)))
56+
return true;
57+
58+
if (typeArg is ITypeParameterSymbol typeParameter)
59+
foreach (var constraint in typeParameter.ConstraintTypes)
60+
if (SymbolEqualityComparer.Default.Equals(constraint, iTheoryDataSymbol) ||
61+
constraint.AllInterfaces.Any(i => SymbolEqualityComparer.Default.Equals(i, iTheoryDataSymbol)))
62+
return true;
63+
64+
return false;
65+
}
66+
}

0 commit comments

Comments
 (0)