diff --git a/src/Directory.Build.props b/src/Directory.Build.props index e38ce07f..67b55bc9 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -103,9 +103,9 @@ - - - + + + diff --git a/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs b/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs index a3fe4576..8b12f1f8 100644 --- a/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs +++ b/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs @@ -321,7 +321,7 @@ public void TestMethod(int _) {{ }} } [Fact] - public async Task ITheoryDataRow_DoesNotTrigger() + public async Task GenericTheoryDataRow_DoesNotTrigger() { var source = @" using System.Collections.Generic; @@ -335,21 +335,21 @@ public class TestClass public void SkippedDataRow(int x, string y) { } - public static List DataRowSource() => - new List() + public static List> DataRowSource() => + new() { - new TheoryDataRow(42, ""Hello, world!""), - new TheoryDataRow(0, null) { Skip = ""Don't run this!"" }, + new(42, ""Hello, world!""), + new(0, null) { Skip = ""Don't run this!"" }, }; }"; - await Verify.VerifyAnalyzerV3(source); + await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp9, source); } [Theory] [InlineData("Task")] [InlineData("ValueTask")] - public async Task Async_ITheoryDataRow_DoesNotTrigger(string taskType) + public async Task Async_GenericTheoryDataRow_DoesNotTrigger(string taskType) { var source = @$" using System.Collections.Generic; @@ -364,19 +364,19 @@ public class TestClass public void SkippedDataRow(int x, string y) {{ }} - public static async {taskType}> DataRowSource() + public static async {taskType}>> DataRowSource() {{ await Task.Yield(); - return new List() + return new() {{ - new TheoryDataRow(42, ""Hello, world!""), - new TheoryDataRow(0, null) {{ Skip = ""Don't run this!"" }}, + new(42, ""Hello, world!""), + new(0, null) {{ Skip = ""Don't run this!"" }}, }}; }} }}"; - await Verify.VerifyAnalyzerV3(source); + await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp9, source); } [Theory] @@ -848,22 +848,52 @@ public async Task ValidTheoryDataMemberWithNotEnoughTypeParameters_Triggers( string memberArgs) { var source = $@" +using Xunit; + public class TestClass {{ - public static Xunit.TheoryData TestData{memberSyntax}new Xunit.TheoryData(); + public static TheoryData TestData{memberSyntax}new TheoryData(); - [Xunit.MemberData(nameof(TestData){memberArgs})] + [MemberData(nameof(TestData){memberArgs})] public void TestMethod(int n, string f) {{ }} }}"; var expected = Verify .Diagnostic("xUnit1037") - .WithSpan(5, 6, 5, 40 + memberArgs.Length) - .WithSeverity(DiagnosticSeverity.Error); + .WithSpan(7, 6, 7, 34 + memberArgs.Length) + .WithSeverity(DiagnosticSeverity.Error) + .WithArguments("Xunit.TheoryData"); await Verify.VerifyAnalyzer(source, expected); } + [Theory] + [MemberData(nameof(MemberSyntaxAndArgs))] + public async Task ValidTheoryDataRowMemberWithNotEnoughTypeParameters_Triggers( + string memberSyntax, + string memberArgs) + { + var source = $@" +using System.Collections.Generic; +using Xunit; + +public class TestClass {{ + public static IEnumerable> TestData{memberSyntax}null; + + [MemberData(nameof(TestData){memberArgs})] + public void TestMethod(int n, string f) {{ }} +}}"; + + var expected = + Verify + .Diagnostic("xUnit1037") + .WithSpan(8, 6, 8, 34 + memberArgs.Length) + .WithSeverity(DiagnosticSeverity.Error) + .WithArguments("Xunit.TheoryDataRow"); + + await Verify.VerifyAnalyzerV3(source, expected); + } + [Theory] [MemberData(nameof(MemberSyntaxAndArgs))] public async Task ValidSubclassedTheoryDataMemberWithNotEnoughTypeParameters_Triggers( @@ -878,18 +908,50 @@ public class DerivedTheoryData : TheoryData {{ }} public class TestClass {{ public static DerivedTheoryData TestData{memberSyntax}new DerivedTheoryData(); - [Xunit.MemberData(nameof(TestData){memberArgs})] + [MemberData(nameof(TestData){memberArgs})] public void TestMethod(int n, string f) {{ }} }}"; var expected = Verify .Diagnostic("xUnit1037") - .WithSpan(9, 6, 9, 40 + memberArgs.Length) - .WithSeverity(DiagnosticSeverity.Error); + .WithSpan(9, 6, 9, 34 + memberArgs.Length) + .WithSeverity(DiagnosticSeverity.Error) + .WithArguments("Xunit.TheoryData"); await Verify.VerifyAnalyzer(source, expected); } + + [Theory] + [MemberData(nameof(MemberSyntaxAndArgs))] + public async Task ValidSubclassedTheoryDataRowMemberWithNotEnoughTypeParameters_Triggers( + string memberSyntax, + string memberArgs) + { + var source = $@" +using System.Collections.Generic; +using Xunit; + +public class DerivedTheoryDataRow : TheoryDataRow {{ + public DerivedTheoryDataRow(T t, U u) : base(t) {{ }} +}} + +public class TestClass {{ + public static List> TestData{memberSyntax}null; + + [MemberData(nameof(TestData){memberArgs})] + public void TestMethod(int n, string f) {{ }} +}}"; + + var expected = + Verify + .Diagnostic("xUnit1037") + .WithSpan(12, 6, 12, 34 + memberArgs.Length) + .WithSeverity(DiagnosticSeverity.Error) + .WithArguments("Xunit.TheoryDataRow"); + + await Verify.VerifyAnalyzerV3(source, expected); + } } public class X1038_MemberDataTheoryDataTypeArgumentsMustMatchTestMethodParameters_ExtraTypeParameters @@ -911,6 +973,23 @@ public class X1038_MemberDataTheoryDataTypeArgumentsMustMatchTestMethodParameter } ); + public static MatrixTheoryData<(string syntax, string args), string> MemberSyntaxAndArgs_WithTheoryDataRowType(string theoryDataTypes) => + new( + new[] + { + ( " = ", "" ), // Field + ( " => ", "" ), // Property + ( "() => ", "" ), // Method w/o args + ( "(int n) => ", ", 42" ), // Method w/ args + }, + new[] + { + $"TheoryDataRow<{theoryDataTypes}>", + "DerivedTheoryDataRow", + $"DerivedTheoryDataRow<{theoryDataTypes}>" + } + ); + [Theory] [MemberData(nameof(MemberSyntaxAndArgs_WithTheoryDataType), "int", DisableDiscoveryEnumeration = true)] public async Task ValidTheoryData_DoesNotTrigger( @@ -933,6 +1012,33 @@ public void TestMethod(int n) {{ }} await Verify.VerifyAnalyzer(source); } + [Theory] + [MemberData(nameof(MemberSyntaxAndArgs_WithTheoryDataRowType), "int", DisableDiscoveryEnumeration = true)] + public async Task ValidTheoryDataRow_DoesNotTrigger( + (string syntax, string args) member, + string theoryDataRowType) + { + var source = $@" +using System.Collections.Generic; +using Xunit; + +public class DerivedTheoryDataRow : TheoryDataRow {{ + public DerivedTheoryDataRow(int value) : base(value) {{ }} +}} +public class DerivedTheoryDataRow : TheoryDataRow {{ + public DerivedTheoryDataRow(T value) : base(value) {{ }} +}} + +public class TestClass {{ + public static IEnumerable<{theoryDataRowType}> TestData{member.syntax}new List<{theoryDataRowType}>(); + + [MemberData(nameof(TestData){member.args})] + public void TestMethod(int n) {{ }} +}}"; + + await Verify.VerifyAnalyzerV3(source); + } + [Theory] [MemberData(nameof(MemberSyntaxAndArgs_WithTheoryDataType), "int", DisableDiscoveryEnumeration = true)] public async Task ValidTheoryDataWithOptionalParameters_DoesNotTrigger( @@ -955,6 +1061,33 @@ public void TestMethod(int n, int a = 0) {{ }} await Verify.VerifyAnalyzer(source); } + [Theory] + [MemberData(nameof(MemberSyntaxAndArgs_WithTheoryDataRowType), "int", DisableDiscoveryEnumeration = true)] + public async Task ValidTheoryDataRowWithOptionalParameters_DoesNotTrigger( + (string syntax, string args) member, + string theoryDataRowType) + { + var source = $@" +using System.Collections.Generic; +using Xunit; + +public class DerivedTheoryDataRow : TheoryDataRow {{ + public DerivedTheoryDataRow(int value) : base(value) {{ }} +}} +public class DerivedTheoryDataRow : TheoryDataRow {{ + public DerivedTheoryDataRow(T value) : base(value) {{ }} +}} + +public class TestClass {{ + public static {theoryDataRowType}[] TestData{member.syntax}new {theoryDataRowType}[0]; + + [MemberData(nameof(TestData){member.args})] + public void TestMethod(int n, int a = 0) {{ }} +}}"; + + await Verify.VerifyAnalyzerV3(source); + } + [Theory] [MemberData(nameof(MemberSyntaxAndArgs_WithTheoryDataType), "int", DisableDiscoveryEnumeration = true)] public async Task ValidTheoryDataWithNoValuesForParamsArray_DoesNotTrigger( @@ -970,13 +1103,40 @@ public class DerivedTheoryData : TheoryData {{ }} public class TestClass {{ public static {theoryDataType} TestData{member.syntax}new {theoryDataType}(); - [Xunit.MemberData(nameof(TestData){member.args})] + [MemberData(nameof(TestData){member.args})] public void TestMethod(int n, params int[] a) {{ }} }}"; await Verify.VerifyAnalyzer(source); } + [Theory] + [MemberData(nameof(MemberSyntaxAndArgs_WithTheoryDataRowType), "int", DisableDiscoveryEnumeration = true)] + public async Task ValidTheoryDataRowWithNoValuesForParamsArray_DoesNotTrigger( + (string syntax, string args) member, + string theoryDataRowType) + { + var source = $@" +using System.Collections.Generic; +using Xunit; + +public class DerivedTheoryDataRow : TheoryDataRow {{ + public DerivedTheoryDataRow(int value) : base(value) {{ }} +}} +public class DerivedTheoryDataRow : TheoryDataRow {{ + public DerivedTheoryDataRow(T value) : base(value) {{ }} +}} + +public class TestClass {{ + public static ICollection<{theoryDataRowType}> TestData{member.syntax}new List<{theoryDataRowType}>(); + + [MemberData(nameof(TestData){member.args})] + public void TestMethod(int n, params int[] a) {{ }} +}}"; + + await Verify.VerifyAnalyzerV3(source); + } + [Theory] [MemberData(nameof(MemberSyntaxAndArgs_WithTheoryDataType), "int, int", DisableDiscoveryEnumeration = true)] public async Task ValidTheoryDataWithSingleValueForParamsArray_DoesNotTrigger( @@ -999,6 +1159,33 @@ public void TestMethod(int n, params int[] a) {{ }} await Verify.VerifyAnalyzer(source); } + [Theory] + [MemberData(nameof(MemberSyntaxAndArgs_WithTheoryDataRowType), "int, int", DisableDiscoveryEnumeration = true)] + public async Task ValidTheoryDataRowWithSingleValueForParamsArray_DoesNotTrigger( + (string syntax, string args) member, + string theoryDataRowType) + { + var source = $@" +using System.Collections.Generic; +using Xunit; + +public class DerivedTheoryDataRow : TheoryDataRow {{ + public DerivedTheoryDataRow(int p1, int p2) : base(p1, p2) {{ }} +}} +public class DerivedTheoryDataRow : TheoryDataRow {{ + public DerivedTheoryDataRow(T1 p1, T2 p2) : base(p1, p2) {{ }} +}} + +public class TestClass {{ + public static IEnumerable<{theoryDataRowType}> TestData{member.syntax}new List<{theoryDataRowType}>(); + + [MemberData(nameof(TestData){member.args})] + public void TestMethod(int n, params int[] a) {{ }} +}}"; + + await Verify.VerifyAnalyzerV3(source); + } + [Theory] [MemberData(nameof(MemberSyntaxAndArgs_WithTheoryDataType), "int", DisableDiscoveryEnumeration = true)] public async Task ValidTheoryDataWithGenericTestParameter_DoesNotTrigger( @@ -1021,6 +1208,33 @@ public void TestMethod(T n) {{ }} await Verify.VerifyAnalyzer(source); } + [Theory] + [MemberData(nameof(MemberSyntaxAndArgs_WithTheoryDataRowType), "int", DisableDiscoveryEnumeration = true)] + public async Task ValidTheoryDataRowWithGenericTestParameter_DoesNotTrigger( + (string syntax, string args) member, + string theoryDataRowType) + { + var source = $@" +using System.Collections.Generic; +using Xunit; + +public class DerivedTheoryDataRow : TheoryDataRow {{ + public DerivedTheoryDataRow(int value) : base(value) {{ }} +}} +public class DerivedTheoryDataRow : TheoryDataRow {{ + public DerivedTheoryDataRow(T value) : base(value) {{ }} +}} + +public class TestClass {{ + public static ISet<{theoryDataRowType}> TestData{member.syntax}new HashSet<{theoryDataRowType}>(); + + [MemberData(nameof(TestData){member.args})] + public void TestMethod(T n) {{ }} +}}"; + + await Verify.VerifyAnalyzerV3(source); + } + [Theory] [MemberData(nameof(MemberSyntaxAndArgs_WithTheoryDataType), "int", DisableDiscoveryEnumeration = true)] public async Task ValidTheoryDataWithNullableGenericTestParameter_DoesNotTrigger( @@ -1045,6 +1259,35 @@ public void TestMethod(T? n) {{ }} await Verify.VerifyAnalyzer(LanguageVersion.CSharp9, source); } + [Theory] + [MemberData(nameof(MemberSyntaxAndArgs_WithTheoryDataRowType), "int", DisableDiscoveryEnumeration = true)] + public async Task ValidTheoryDataRowWithNullableGenericTestParameter_DoesNotTrigger( + (string syntax, string args) member, + string theoryDataRowType) + { + var source = $@" +#nullable enable + +using System.Collections.Generic; +using Xunit; + +public class DerivedTheoryDataRow : TheoryDataRow {{ + public DerivedTheoryDataRow(int value) : base(value) {{ }} +}} +public class DerivedTheoryDataRow : TheoryDataRow {{ + public DerivedTheoryDataRow(T value) : base(value) {{ }} +}} + +public class TestClass {{ + public static IEnumerable<{theoryDataRowType}> TestData{member.syntax}new List<{theoryDataRowType}>(); + + [Xunit.MemberData(nameof(TestData){member.args})] + public void TestMethod(T? n) {{ }} +}}"; + + await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp9, source); + } + [Theory] [InlineData(" = ", "")] // Field [InlineData(" => ", "")] // Property @@ -1069,6 +1312,33 @@ public void TestMethod(int n) {{ }} await Verify.VerifyAnalyzer(source); } + [Theory] + [InlineData(" = ", "")] // Field + [InlineData(" => ", "")] // Property + [InlineData("() => ", "")] // Method w/o args + [InlineData("(int n) => ", ", 42")] // Method w/ args + public async Task ValidTheoryDataRowDoubleGenericSubclassMember_DoesNotTrigger( + string memberSyntax, + string memberArgs) + { + var source = $@" +using System.Collections.Generic; +using Xunit; + +public class DerivedTheoryDataRow : TheoryDataRow {{ + public DerivedTheoryDataRow(T t, U u) : base(t) {{ }} +}} + +public class TestClass {{ + public static IList> TestData{memberSyntax}new List>(); + + [MemberData(nameof(TestData){memberArgs})] + public void TestMethod(int n) {{ }} +}}"; + + await Verify.VerifyAnalyzerV3(source); + } + [Fact] public async Task WithIntArrayArguments_DoesNotTrigger() { @@ -1113,14 +1383,49 @@ public void TestMethod(int n) {{ }} Verify .Diagnostic("xUnit1038") .WithSpan(10, 6, 10, 34 + member.args.Length) - .WithSeverity(DiagnosticSeverity.Error); + .WithSeverity(DiagnosticSeverity.Error) + .WithArguments("Xunit.TheoryData"); await Verify.VerifyAnalyzer(source, expected); } + [Theory] + [MemberData(nameof(MemberSyntaxAndArgs_WithTheoryDataRowType), "int, string", DisableDiscoveryEnumeration = true)] + public async Task ValidSubclassTheoryDataRowMemberWithTooManyTypeParameters_Triggers( + (string syntax, string args) member, + string theoryDataRowType) + { + var source = $@" +using System.Collections.Generic; +using Xunit; + +public class DerivedTheoryDataRow : TheoryDataRow {{ + public DerivedTheoryDataRow(int p1, string p2) : base(p1, p2) {{ }} +}} +public class DerivedTheoryDataRow : TheoryDataRow {{ + public DerivedTheoryDataRow(T1 p1, T2 p2) : base(p1, p2) {{ }} +}} + +public class TestClass {{ + public static IEnumerable<{theoryDataRowType}> TestData{member.syntax}new List<{theoryDataRowType}>(); + + [MemberData(nameof(TestData){member.args})] + public void TestMethod(int n) {{ }} +}}"; + + var expected = + Verify + .Diagnostic("xUnit1038") + .WithSpan(15, 6, 15, 34 + member.args.Length) + .WithSeverity(DiagnosticSeverity.Error) + .WithArguments("Xunit.TheoryDataRow"); + + await Verify.VerifyAnalyzerV3(source, expected); + } + [Theory] [MemberData(nameof(MemberSyntaxAndArgs_WithTheoryDataType), "int, string[], string", DisableDiscoveryEnumeration = true)] - public async Task ExtraTypeExistsPastArrayForParamsArray_Triggers( + public async Task ExtraTheoryDataTypeExistsPastArrayForParamsArray_Triggers( (string syntax, string args) member, string theoryDataType) { @@ -1131,7 +1436,7 @@ public class DerivedTheoryData : TheoryData {{ }} public class DerivedTheoryData : TheoryData {{ }} public class TestClass {{ - public static {theoryDataType} TestData{member.syntax}new {theoryDataType}(); + public static {theoryDataType} TestData{member.syntax}new {theoryDataType}(); [MemberData(nameof(TestData){member.args})] public void PuzzleOne(int _1, params string[] _2) {{ }} @@ -1141,10 +1446,45 @@ public void PuzzleOne(int _1, params string[] _2) {{ }} Verify .Diagnostic("xUnit1038") .WithSpan(10, 6, 10, 34 + member.args.Length) - .WithSeverity(DiagnosticSeverity.Error); + .WithSeverity(DiagnosticSeverity.Error) + .WithArguments("Xunit.TheoryData"); await Verify.VerifyAnalyzer(source, expected); } + + [Theory] + [MemberData(nameof(MemberSyntaxAndArgs_WithTheoryDataRowType), "int, string[], string", DisableDiscoveryEnumeration = true)] + public async Task ExtraTheoryDataRowTypeExistsPastArrayForParamsArray_Triggers( + (string syntax, string args) member, + string theoryDataType) + { + var source = $@" +using System.Collections.Generic; +using Xunit; + +public class DerivedTheoryDataRow : TheoryDataRow {{ + public DerivedTheoryDataRow(int p1, string[] p2, string p3) : base(p1, p2, p3) {{ }} +}} +public class DerivedTheoryDataRow : TheoryDataRow {{ + public DerivedTheoryDataRow(T1 p1, T2 p2, T3 p3) : base(p1, p2, p3) {{ }} +}} + +public class TestClass {{ + public static ICollection<{theoryDataType}> TestData{member.syntax}new {theoryDataType}[0]; + + [MemberData(nameof(TestData){member.args})] + public void PuzzleOne(int _1, params string[] _2) {{ }} +}}"; + + var expected = + Verify + .Diagnostic("xUnit1038") + .WithSpan(15, 6, 15, 34 + member.args.Length) + .WithSeverity(DiagnosticSeverity.Error) + .WithArguments("Xunit.TheoryDataRow"); + + await Verify.VerifyAnalyzerV3(source, expected); + } } public class X1039_MemberDataTheoryDataTypeArgumentsMustMatchTestMethodParameters_IncompatibleTypes @@ -1166,7 +1506,7 @@ public class X1039_MemberDataTheoryDataTypeArgumentsMustMatchTestMethodParameter ); [Fact] - public async Task DoesNotFindWarning_WhenPassingMultipleValuesForParamsArray() + public async Task WhenPassingMultipleValuesForParamsArray_TheoryData_DoesNotTrigger() { var source = @" using Xunit; @@ -1182,7 +1522,24 @@ public void PuzzleOne(int _1, params string[] _2) { } } [Fact] - public async Task DoesNotFindWarning_WhenPassingArrayForParamsArray() + public async Task WhenPassingMultipleValuesForParamsArray_TheoryDataRow_DoesNotTrigger() + { + var source = @" +using System.Collections.Generic; +using Xunit; + +public class TestClass { + public static IEnumerable> TestData = new List>(); + + [MemberData(nameof(TestData))] + public void PuzzleOne(int _1, params string[] _2) { } +}"; + + await Verify.VerifyAnalyzerV3(source); + } + + [Fact] + public async Task WhenPassingArrayForParamsArray_TheoryData_DoesNotTrigger() { var source = @" using Xunit; @@ -1198,7 +1555,23 @@ public void PuzzleOne(int _1, params string[] _2) { } } [Fact] - public async Task DoesNotFindWarning_WhenPassingTupleWithoutFieldNames() + public async Task WhenPassingArrayForParamsArray_TheoryDataRow_DoesNotTrigger() + { + var source = @" +using Xunit; + +public class TestClass { + public static TheoryDataRow[] TestData = new TheoryDataRow[0]; + + [MemberData(nameof(TestData))] + public void PuzzleOne(int _1, params string[] _2) { } +}"; + + await Verify.VerifyAnalyzerV3(source); + } + + [Fact] + public async Task WhenPassingTupleWithoutFieldNames_TheoryData_DoesNotTrigger() { var source = @" using Xunit; @@ -1214,7 +1587,24 @@ public void TestMethod((int a, int b) x) { } } [Fact] - public async Task DoesNotFindWarning_WhenPassingTupleWithDifferentFieldNames() + public async Task WhenPassingTupleWithoutFieldNames_TheoryDataRow_DoesNotTrigger() + { + var source = @" +using System.Collections.Generic; +using Xunit; + +public class TestClass { + public static IList> TestData = new List>(); + + [MemberData(nameof(TestData))] + public void TestMethod((int a, int b) x) { } +}"; + + await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp8, source); + } + + [Fact] + public async Task WhenPassingTupleWithDifferentFieldNames_TheoryData_DoesNotTrigger() { var source = @" using Xunit; @@ -1230,7 +1620,24 @@ public void TestMethod((int a, int b) x) { } } [Fact] - public async Task FindWarning_WithExtraValueNotCompatibleWithParamsArray() + public async Task WhenPassingTupleWithDifferentFieldNames_TheoryDataRow_DoesNotTrigger() + { + var source = @" +using System.Collections.Generic; +using Xunit; + +public class TestClass { + public static IEnumerable> TestData = new List>(); + + [MemberData(nameof(TestData))] + public void TestMethod((int a, int b) x) { } +}"; + + await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp8, source); + } + + [Fact] + public async Task WithExtraValueNotCompatibleWithParamsArray_TheoryData_Triggers() { var source = @" using Xunit; @@ -1252,9 +1659,33 @@ public void PuzzleOne(int _1, params string[] _2) { } await Verify.VerifyAnalyzer(source, expected); } + [Fact] + public async Task WithExtraValueNotCompatibleWithParamsArray_TheoryDataRow_Triggers() + { + var source = @" +using System.Collections.Generic; +using Xunit; + +public class TestClass { + public static IEnumerable> TestData = new List>(); + + [MemberData(nameof(TestData))] + public void PuzzleOne(int _1, params string[] _2) { } +}"; + + var expected = + Verify + .Diagnostic("xUnit1039") + .WithSpan(9, 42, 9, 50) + .WithSeverity(DiagnosticSeverity.Error) + .WithArguments("int", "TestClass", "TestData", "_2"); + + await Verify.VerifyAnalyzerV3(source, expected); + } + [Theory] [MemberData(nameof(TypeWithMemberSyntaxAndArgs), DisableDiscoveryEnumeration = true)] - public async Task FindWarning_IfHasValidTheoryDataMemberWithIncompatibleTypeParameters( + public async Task ValidTheoryDataMemberWithIncompatibleTypeParameters_Triggers( (string syntax, string args) member, string type) { @@ -1277,6 +1708,33 @@ public void TestMethod(string f) {{ }} await Verify.VerifyAnalyzer(source, expected); } + + [Theory] + [MemberData(nameof(TypeWithMemberSyntaxAndArgs), DisableDiscoveryEnumeration = true)] + public async Task ValidTheoryDataRowMemberWithIncompatibleTypeParameters_Triggers( + (string syntax, string args) member, + string type) + { + var source = $@" +using System.Collections.Generic; +using Xunit; + +public class TestClass {{ + public static IList> TestData{member.syntax}new List>(); + + [MemberData(nameof(TestData){member.args})] + public void TestMethod(string f) {{ }} +}}"; + + var expected = + Verify + .Diagnostic("xUnit1039") + .WithSpan(9, 28, 9, 34) + .WithSeverity(DiagnosticSeverity.Error) + .WithArguments(type, "TestClass", "TestData", "f"); + + await Verify.VerifyAnalyzerV3(source, expected); + } } public class X1040_MemberDataTheoryDataTypeArgumentsMustMatchTestMethodParameters_IncompatibleNullability @@ -1316,6 +1774,35 @@ public void TestMethod(string f) {{ }} await Verify.VerifyAnalyzer(LanguageVersion.CSharp8, source, expected); } + + [Theory] + [MemberData(nameof(MemberSyntaxAndArgs))] + public async Task ValidTheoryDataRowMemberWithMismatchedNullability_Triggers( + string memberSyntax, + string memberArgs) + { + var source = $@" +#nullable enable + +using System.Collections.Generic; +using Xunit; + +public class TestClass {{ + public static IEnumerable> TestData{memberSyntax}new List>(); + + [MemberData(nameof(TestData){memberArgs})] + public void TestMethod(string f) {{ }} +}}"; + + var expected = + Verify + .Diagnostic("xUnit1040") + .WithSpan(11, 28, 11, 34) + .WithSeverity(DiagnosticSeverity.Warning) + .WithArguments("string?", "TestClass", "TestData", "f"); + + await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp8, source, expected); + } } public class X1042_MemberDataTheoryDataIsRecommendedForStronglyTypedAnalysis @@ -1355,10 +1842,10 @@ public void TestMethod(int _1, string _2) { } } [Theory] - [InlineData("IEnumerable")] - [InlineData("List")] - [InlineData("ITheoryDataRow[]")] - public async Task TheoryDataRow_DoesNotTrigger(string memberType) + [InlineData("IEnumerable>")] + [InlineData("List>")] + [InlineData("TheoryDataRow[]")] + public async Task GenericTheoryDataRow_DoesNotTrigger(string memberType) { var source = $@" using System.Collections.Generic; @@ -1391,13 +1878,21 @@ public class TestClass {{ public void TestMethod(int _) {{ }} }}"; - var expected = + var expectedV2 = Verify .Diagnostic("xUnit1042") .WithSpan(8, 6, 8, 30) - .WithSeverity(DiagnosticSeverity.Info); + .WithSeverity(DiagnosticSeverity.Info) + .WithArguments("TheoryData<>"); + var expectedV3 = + Verify + .Diagnostic("xUnit1042") + .WithSpan(8, 6, 8, 30) + .WithSeverity(DiagnosticSeverity.Info) + .WithArguments("TheoryData<> or IEnumerable>"); - await Verify.VerifyAnalyzer(source, expected); + await Verify.VerifyAnalyzerV2(source, expectedV2); + await Verify.VerifyAnalyzerV3(source, expectedV3); } // For v2, we test for xUnit1019 above, since it's incompatible rather than "compatible, @@ -1405,9 +1900,16 @@ public void TestMethod(int _) {{ }} [Theory] [InlineData("Task>")] [InlineData("ValueTask>")] - public async Task Async_ValidTypesWhichAreNotTheoryData_TriggersInV3(string memberType) + [InlineData("IEnumerable")] + [InlineData("Task>")] + [InlineData("ValueTask>")] + [InlineData("IEnumerable")] + [InlineData("Task>")] + [InlineData("ValueTask")] + public async Task ValidTypesWhichAreNotTheoryDataOrGenericTheoryDataRow_TriggersInV3(string memberType) { var source = $@" +using System.Collections; using System.Collections.Generic; using System.Threading.Tasks; using Xunit; @@ -1417,15 +1919,22 @@ public class TestClass {{ [MemberData(nameof(Data))] public void TestMethod(int _) {{ }} +}} + +public class EnumerableOfITheoryDataRow : IEnumerable +{{ + public IEnumerator GetEnumerator() => null; + IEnumerator IEnumerable.GetEnumerator() => null; }}"; - var expected = + var expectedV2 = Verify .Diagnostic("xUnit1042") - .WithSpan(9, 6, 9, 30) - .WithSeverity(DiagnosticSeverity.Info); + .WithSpan(10, 6, 10, 30) + .WithSeverity(DiagnosticSeverity.Info) + .WithArguments("TheoryData<> or IEnumerable>"); - await Verify.VerifyAnalyzerV3(source, expected); + await Verify.VerifyAnalyzerV3(source, expectedV2); } } } diff --git a/src/xunit.analyzers.tests/Analyzers/X1000/TheoryDataRowArgumentsShouldBeSerializableTests.cs b/src/xunit.analyzers.tests/Analyzers/X1000/TheoryDataRowArgumentsShouldBeSerializableTests.cs index f59f7440..f2a45714 100644 --- a/src/xunit.analyzers.tests/Analyzers/X1000/TheoryDataRowArgumentsShouldBeSerializableTests.cs +++ b/src/xunit.analyzers.tests/Analyzers/X1000/TheoryDataRowArgumentsShouldBeSerializableTests.cs @@ -60,7 +60,7 @@ public async Task IntrinsicallySerializableValue_DoesNotTrigger( using System; using System.Collections.Generic; using System.Numerics; -using Xunit.Sdk; +using Xunit; public class MyClass {{ public IEnumerable MyMethod() {{ @@ -70,7 +70,7 @@ public IEnumerable MyMethod() {{ var arrayValue = new {type}[0]; yield return new TheoryDataRow(value, defaultValue, nullValue, arrayValue); - yield return new TheoryDataRow({value}, default({type}), default({type}?), new {type}[0]); + yield return new TheoryDataRow<{type}, {type}?, {type}?, {type}[]>({value}, default({type}), default({type}?), new {type}[0]); }} }}"; @@ -86,6 +86,7 @@ public async Task IXunitSerializableValue_DoesNotTrigger(string type) #nullable enable using System.Collections.Generic; +using Xunit; using Xunit.Sdk; public class MyClass {{ @@ -96,7 +97,7 @@ public IEnumerable MyMethod() {{ var arrayValue = new {type}[0]; yield return new TheoryDataRow(value, defaultValue, nullValue, arrayValue); - yield return new TheoryDataRow(new {type}(), default({type}), default({type}?), new {type}[0]); + yield return new TheoryDataRow<{type}, {type}?, {type}?, {type}[]>(new {type}(), default({type}), default({type}?), new {type}[0]); }} }} @@ -130,7 +131,7 @@ public async Task KnownNonSerializableValue_Triggers1046( using System; using System.Collections.Generic; -using Xunit.Sdk; +using Xunit; public class MyClass {{ public IEnumerable MyMethod() {{ @@ -139,7 +140,7 @@ public IEnumerable MyMethod() {{ var arrayValue = new {type}[0]; yield return new TheoryDataRow(defaultValue, nullValue, arrayValue); - yield return new TheoryDataRow(default({type}), default({type}?), new {type}[0]); + yield return new TheoryDataRow<{defaultValueType}, {nullValueType}, {type}[]>(default({type}), default({type}?), new {type}[0]); }} }} @@ -147,6 +148,7 @@ public sealed class NonSerializableSealedClass {{ }} public struct NonSerializableStruct {{ }}"; + var leftGeneric = 48 + defaultValueType.Length + nullValueType.Length + type.Length; var expected = new[] { Verify .Diagnostic("xUnit1046") @@ -162,21 +164,69 @@ public struct NonSerializableStruct {{ }}"; .WithArguments("arrayValue", $"{type}[]"), Verify .Diagnostic("xUnit1046") - .WithSpan(15, 40, 15, 49 + type.Length) + .WithSpan(15, leftGeneric, 15, leftGeneric + 9 + type.Length) // +9 for "default()" .WithArguments($"default({type})", defaultValueType), Verify .Diagnostic("xUnit1046") - .WithSpan(15, 51 + type.Length, 15, 61 + type.Length * 2) + .WithSpan(15, leftGeneric + 11 + type.Length, 15, leftGeneric + 21 + type.Length * 2) // +10 for "default(?)" .WithArguments($"default({type}?)", nullValueType), Verify .Diagnostic("xUnit1046") - .WithSpan(15, 63 + type.Length * 2, 15, 70 + type.Length * 3) + .WithSpan(15, leftGeneric + 23 + type.Length * 2, 15, leftGeneric + 30 + type.Length * 3) // +7 for "new [0]" .WithArguments($"new {type}[0]", $"{type}[]"), }; await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp8, source, expected); } + [Theory] + [InlineData("NonSerializableSealedClass")] + [InlineData("NonSerializableStruct")] + public async Task KnownNonSerializableValue_Constructable_Triggers1046(string type) + { + var source = $@" +#nullable enable + +using System.Collections.Generic; +using Xunit; + +public class MyClass {{ + public IEnumerable MyMethod() {{ + var value = new {type}(); + + yield return new TheoryDataRow(value); + yield return new TheoryDataRow(new {type}()); + yield return new TheoryDataRow<{type}>(value); + yield return new TheoryDataRow<{type}>(new {type}()); + }} +}} + +public sealed class NonSerializableSealedClass {{ }} + +public struct NonSerializableStruct {{ }}"; + + var expected = new[] { + Verify + .Diagnostic("xUnit1046") + .WithSpan(11, 40, 11, 45) + .WithArguments("value", type), + Verify + .Diagnostic("xUnit1046") + .WithSpan(12, 40, 12, 46 + type.Length) + .WithArguments($"new {type}()", type), + Verify + .Diagnostic("xUnit1046") + .WithSpan(13, 42 + type.Length, 13, 47 + type.Length) + .WithArguments("value", type), + Verify + .Diagnostic("xUnit1046") + .WithSpan(14, 42 + type.Length, 14, 48 + type.Length * 2) + .WithArguments($"new {type}()", type), + }; + + await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp8, source, expected); + } + [Theory] [InlineData("object")] [InlineData("Array")] @@ -194,7 +244,7 @@ public async Task MaybeNonSerializableValue_Triggers1047(string type) using System; using System.Collections; using System.Collections.Generic; -using Xunit.Sdk; +using Xunit; public class MyClass {{ public IEnumerable MyMethod() {{ @@ -203,7 +253,7 @@ public IEnumerable MyMethod() {{ var arrayValue = new {type}[0]; yield return new TheoryDataRow(defaultValue, nullValue, arrayValue); - yield return new TheoryDataRow(default({type}), default({type}?), new {type}[0]); + yield return new TheoryDataRow<{type}, {type}, {type}[]>(default({type}), default({type}?), new {type}[0]); }} }} @@ -211,6 +261,7 @@ public interface IPossiblySerializableInterface {{ }} public class PossiblySerializableUnsealedClass {{ }}"; + var leftGeneric = 48 + type.Length * 3; var expected = new[] { Verify .Diagnostic("xUnit1047") @@ -226,18 +277,65 @@ public class PossiblySerializableUnsealedClass {{ }}"; .WithArguments("arrayValue", $"{type}[]"), Verify .Diagnostic("xUnit1047") - .WithSpan(16, 40, 16, 49 + type.Length) + .WithSpan(16, leftGeneric, 16, leftGeneric + 9 + type.Length) // +9 for "default()" .WithArguments($"default({type})", $"{type}?"), Verify .Diagnostic("xUnit1047") - .WithSpan(16, 51 + type.Length, 16, 61 + type.Length * 2) + .WithSpan(16, leftGeneric + 11 + type.Length, 16, leftGeneric + 21 + type.Length * 2) // +10 for "default(?)" .WithArguments($"default({type}?)", $"{type}?"), Verify .Diagnostic("xUnit1047") - .WithSpan(16, 63 + type.Length * 2, 16, 70 + type.Length * 3) + .WithSpan(16, leftGeneric + 23 + type.Length * 2, 16, leftGeneric + 30 + type.Length * 3) // +7 for "new [0]" .WithArguments($"new {type}[0]", $"{type}[]"), }; await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp8, source, expected); } + + [Theory] + [InlineData("object")] + [InlineData("Dictionary")] + [InlineData("PossiblySerializableUnsealedClass")] + public async Task MaybeNonSerializableValue_Constructable_Triggers1047(string type) + { + var source = $@" +#nullable enable + +using System.Collections.Generic; +using Xunit; + +public class MyClass {{ + public IEnumerable MyMethod() {{ + var value = new {type}(); + + yield return new TheoryDataRow(value); + yield return new TheoryDataRow(new {type}()); + yield return new TheoryDataRow<{type}>(value); + yield return new TheoryDataRow<{type}>(new {type}()); + }} +}} + +public class PossiblySerializableUnsealedClass {{ }}"; + + var expected = new[] { + Verify + .Diagnostic("xUnit1047") + .WithSpan(11, 40, 11, 45) + .WithArguments("value", type), + Verify + .Diagnostic("xUnit1047") + .WithSpan(12, 40, 12, 46 + type.Length) + .WithArguments($"new {type}()", type), + Verify + .Diagnostic("xUnit1047") + .WithSpan(13, 42 + type.Length, 13, 47 + type.Length) + .WithArguments("value", type), + Verify + .Diagnostic("xUnit1047") + .WithSpan(14, 42 + type.Length, 14, 48 + type.Length * 2) + .WithArguments($"new {type}()", type), + }; + + await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp8, source, expected); + } } diff --git a/src/xunit.analyzers.tests/Fixes/X1000/MemberDataShouldReferenceValidMember_ReturnTypeFixerTests.cs b/src/xunit.analyzers.tests/Fixes/X1000/MemberDataShouldReferenceValidMember_ReturnTypeFixerTests.cs index 774d7881..383b9ca4 100644 --- a/src/xunit.analyzers.tests/Fixes/X1000/MemberDataShouldReferenceValidMember_ReturnTypeFixerTests.cs +++ b/src/xunit.analyzers.tests/Fixes/X1000/MemberDataShouldReferenceValidMember_ReturnTypeFixerTests.cs @@ -58,7 +58,7 @@ public class TestClass { public static IEnumerable Data => null; [Theory] - [MemberData(nameof(Data))] + [{|xUnit1042:MemberData(nameof(Data))|}] public void TestMethod(int a) { } }"; diff --git a/src/xunit.analyzers.tests/Utility/CodeAnalyzerHelper.cs b/src/xunit.analyzers.tests/Utility/CodeAnalyzerHelper.cs index 5e09e08c..0a7d7ad1 100644 --- a/src/xunit.analyzers.tests/Utility/CodeAnalyzerHelper.cs +++ b/src/xunit.analyzers.tests/Utility/CodeAnalyzerHelper.cs @@ -38,8 +38,8 @@ static CodeAnalyzerHelper() new PackageIdentity("System.Collections.Immutable", "1.6.0"), new PackageIdentity("System.Threading.Tasks.Extensions", "4.5.4"), new PackageIdentity("xunit.abstractions", "2.0.3"), - new PackageIdentity("xunit.assert", "2.6.7-pre.4"), - new PackageIdentity("xunit.core", "2.6.7-pre.4") + new PackageIdentity("xunit.assert", "2.8.1-pre.14"), + new PackageIdentity("xunit.core", "2.8.1-pre.14") ) ); @@ -49,7 +49,7 @@ static CodeAnalyzerHelper() new PackageIdentity("System.Collections.Immutable", "1.6.0"), new PackageIdentity("System.Threading.Tasks.Extensions", "4.5.4"), new PackageIdentity("xunit.abstractions", "2.0.3"), - new PackageIdentity("xunit.runner.utility", "2.6.7-pre.4") + new PackageIdentity("xunit.runner.utility", "2.8.1-pre.14") ) ); @@ -59,9 +59,9 @@ static CodeAnalyzerHelper() new PackageIdentity("Microsoft.Extensions.Primitives", "8.0.0"), new PackageIdentity("System.Threading.Tasks.Extensions", "4.5.4"), new PackageIdentity("System.Text.Json", "8.0.0"), - new PackageIdentity("xunit.v3.assert", "0.1.1-pre.350"), - new PackageIdentity("xunit.v3.common", "0.1.1-pre.350"), - new PackageIdentity("xunit.v3.extensibility.core", "0.1.1-pre.350") + new PackageIdentity("xunit.v3.assert", "0.1.1-pre.428"), + new PackageIdentity("xunit.v3.common", "0.1.1-pre.428"), + new PackageIdentity("xunit.v3.extensibility.core", "0.1.1-pre.428") ) ); @@ -71,8 +71,8 @@ static CodeAnalyzerHelper() new PackageIdentity("Microsoft.Extensions.Primitives", "8.0.0"), new PackageIdentity("System.Threading.Tasks.Extensions", "4.5.4"), new PackageIdentity("System.Text.Json", "8.0.0"), - new PackageIdentity("xunit.v3.common", "0.1.1-pre.350"), - new PackageIdentity("xunit.v3.runner.utility", "0.1.1-pre.350") + new PackageIdentity("xunit.v3.common", "0.1.1-pre.428"), + new PackageIdentity("xunit.v3.runner.utility", "0.1.1-pre.428") ) ); } diff --git a/src/xunit.analyzers.tests/xunit.analyzers.tests.csproj b/src/xunit.analyzers.tests/xunit.analyzers.tests.csproj index a6d47caf..f13380ef 100644 --- a/src/xunit.analyzers.tests/xunit.analyzers.tests.csproj +++ b/src/xunit.analyzers.tests/xunit.analyzers.tests.csproj @@ -12,12 +12,12 @@ - - - - - - + + + + + + diff --git a/src/xunit.analyzers/Utility/Descriptors.xUnit1xxx.cs b/src/xunit.analyzers/Utility/Descriptors.xUnit1xxx.cs index 9bc87b22..e3551cd0 100644 --- a/src/xunit.analyzers/Utility/Descriptors.xUnit1xxx.cs +++ b/src/xunit.analyzers/Utility/Descriptors.xUnit1xxx.cs @@ -342,25 +342,25 @@ public static partial class Descriptors public static DiagnosticDescriptor X1037_MemberDataTheoryDataTypeArgumentsMustMatchTestMethodParameters_TooFewTypeParameters { get; } = Diagnostic( "xUnit1037", - "There are fewer TheoryData type arguments than required by the parameters of the test method", + "There are fewer theory data type arguments than required by the parameters of the test method", Usage, Error, - "There are fewer TheoryData type arguments than required by the parameters of the test method. Add more type parameters to match the method signature, or remove parameters from the test method." + "There are fewer {0} type arguments than required by the parameters of the test method. Add more type parameters to match the method signature, or remove parameters from the test method." ); public static DiagnosticDescriptor X1038_MemberDataTheoryDataTypeArgumentsMustMatchTestMethodParameters_ExtraTypeParameters { get; } = Diagnostic( "xUnit1038", - "There are more TheoryData type arguments than allowed by the parameters of the test method", + "There are more theory data type arguments than allowed by the parameters of the test method", Usage, Error, - "There are more TheoryData type arguments than allowed by the parameters of the test method. Remove unused type arguments, or add more parameters." + "There are more {0} type arguments than allowed by the parameters of the test method. Remove unused type arguments, or add more parameters." ); public static DiagnosticDescriptor X1039_MemberDataTheoryDataTypeArgumentsMustMatchTestMethodParameters_IncompatibleTypes { get; } = Diagnostic( "xUnit1039", - "The type argument to TheoryData is not compatible with the type of the corresponding test method parameter", + "The type argument to theory data is not compatible with the type of the corresponding test method parameter", Usage, Error, "The type argument {0} from {1}.{2} is not compatible with the type of the corresponding test method parameter {3}." @@ -369,7 +369,7 @@ public static partial class Descriptors public static DiagnosticDescriptor X1040_MemberDataTheoryDataTypeArgumentsMustMatchTestMethodParameters_IncompatibleNullability { get; } = Diagnostic( "xUnit1040", - "The type argument to TheoryData is nullable, while the type of the corresponding test method parameter is not", + "The type argument to theory data is nullable, while the type of the corresponding test method parameter is not", Usage, Warning, "The type argument {0} from {1}.{2} is nullable, while the type of the corresponding test method parameter {3} is not. Make the TheoryData type non-nullable, or make the test method parameter nullable." @@ -390,7 +390,7 @@ public static partial class Descriptors "The member referenced by the MemberData attribute returns untyped data rows", Usage, Info, - "The member referenced by the MemberData attribute returns untyped data rows, such as object[]. Consider using TheoryData<> as the return type to provide better type safety." + "The member referenced by the MemberData attribute returns untyped data rows, such as object[]. Consider using {0} as the return type to provide better type safety." ); public static DiagnosticDescriptor X1043_ConstructorOnFactAttributeSubclassShouldBePublic { get; } = diff --git a/src/xunit.analyzers/Utility/SerializableTypeSymbols.cs b/src/xunit.analyzers/Utility/SerializableTypeSymbols.cs index 1253b051..2071713e 100644 --- a/src/xunit.analyzers/Utility/SerializableTypeSymbols.cs +++ b/src/xunit.analyzers/Utility/SerializableTypeSymbols.cs @@ -7,11 +7,10 @@ namespace Xunit.Analyzers; public sealed class SerializableTypeSymbols { readonly Lazy bigInteger; - readonly Compilation compilation; readonly Lazy dateOnly; readonly Lazy dateTimeOffset; readonly Lazy iXunitSerializable; - readonly Dictionary theoryDataTypes; + readonly Dictionary theoryDataTypes; readonly Lazy timeOnly; readonly Lazy timeSpan; readonly Lazy traitDictionary; @@ -23,15 +22,15 @@ public sealed class SerializableTypeSymbols INamedTypeSymbol classDataAttribute, INamedTypeSymbol dataAttribute, INamedTypeSymbol memberDataAttribute, - INamedTypeSymbol theoryAttribute) + INamedTypeSymbol theoryAttribute, + Dictionary theoryDataTypes) { - this.compilation = compilation; + this.theoryDataTypes = theoryDataTypes; bigInteger = new(() => TypeSymbolFactory.BigInteger(compilation)); dateOnly = new(() => TypeSymbolFactory.DateOnly(compilation)); dateTimeOffset = new(() => TypeSymbolFactory.DateTimeOffset(compilation)); type = new(() => TypeSymbolFactory.Type(compilation)); - theoryDataTypes = []; timeOnly = new(() => TypeSymbolFactory.TimeOnly(compilation)); timeSpan = new(() => TypeSymbolFactory.TimeSpan(compilation)); traitDictionary = new(() => GetTraitDictionary(compilation)); @@ -82,7 +81,8 @@ public sealed class SerializableTypeSymbols classDataAttribute, dataAttribute, memberDataAttribute, - theoryAttribute + theoryAttribute, + TypeSymbolFactory.TheoryData_ByGenericArgumentCount(compilation) ); } @@ -101,14 +101,7 @@ public sealed class SerializableTypeSymbols public INamedTypeSymbol? TheoryData(int arity) { - if (!theoryDataTypes.ContainsKey(arity)) - { - if (arity == 0) - theoryDataTypes[arity] = TypeSymbolFactory.TheoryData(compilation); - else - theoryDataTypes[arity] = TypeSymbolFactory.TheoryDataN(compilation, arity); - } - - return theoryDataTypes[arity]; + theoryDataTypes.TryGetValue(arity, out var result); + return result; } } diff --git a/src/xunit.analyzers/Utility/TypeSymbolFactory.cs b/src/xunit.analyzers/Utility/TypeSymbolFactory.cs index d7b2c4f2..b710cadc 100644 --- a/src/xunit.analyzers/Utility/TypeSymbolFactory.cs +++ b/src/xunit.analyzers/Utility/TypeSymbolFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using Microsoft.CodeAnalysis; @@ -243,13 +244,51 @@ public static INamedTypeSymbol String(Compilation compilation) => public static INamedTypeSymbol? TheoryData(Compilation compilation) => Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("Xunit.TheoryData"); - public static INamedTypeSymbol? TheoryDataN( - Compilation compilation, - int n) => - Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("Xunit.TheoryData`" + n.ToString(CultureInfo.InvariantCulture)); + // Centralized here so we don't repeat knowledge of how many arities exist + // (in case we decide to add more later). + public static Dictionary TheoryData_ByGenericArgumentCount(Compilation compilation) + { + var result = new Dictionary(); + + var type = TheoryData(compilation); + if (type is not null) + result[0] = type; + + for (int i = 1; i <= 10; i++) + { + type = compilation.GetTypeByMetadataName("Xunit.TheoryData`" + i.ToString(CultureInfo.InvariantCulture)); + if (type is not null) + result[i] = type; + } + + return result; + } + // Namespace fallback for builds before TheoryDataRow was moved from Xunit.Sdk to Xunit, should + // eventually be able to get rid of this fallback once v3 goes 1.0. public static INamedTypeSymbol? TheoryDataRow(Compilation compilation) => - Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("Xunit.Sdk.TheoryDataRow"); + Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("Xunit.TheoryDataRow") + ?? compilation.GetTypeByMetadataName("Xunit.Sdk.TheoryDataRow"); + + // Centralized here so we don't repeat knowledge of how many arities exist + // (in case we decide to add more later). + public static Dictionary TheoryDataRow_ByGenericArgumentCount(Compilation compilation) + { + var result = new Dictionary(); + + var type = TheoryDataRow(compilation); + if (type is not null) + result[0] = type; + + for (int i = 1; i <= 10; i++) + { + type = compilation.GetTypeByMetadataName("Xunit.TheoryDataRow`" + i.ToString(CultureInfo.InvariantCulture)); + if (type is not null) + result[i] = type; + } + + return result; + } public static INamedTypeSymbol? TimeOnly(Compilation compilation) => Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("System.TimeOnly"); diff --git a/src/xunit.analyzers/X1000/MemberDataShouldReferenceValidMember.cs b/src/xunit.analyzers/X1000/MemberDataShouldReferenceValidMember.cs index 84a89cc7..ccedbe2f 100644 --- a/src/xunit.analyzers/X1000/MemberDataShouldReferenceValidMember.cs +++ b/src/xunit.analyzers/X1000/MemberDataShouldReferenceValidMember.cs @@ -48,14 +48,8 @@ public override void AnalyzeCompilation( return; var compilation = context.Compilation; - - Dictionary theoryDataTypes = new(); - for (int i = 1; i <= 10; i++) - { - var symbol = TypeSymbolFactory.TheoryDataN(compilation, i); - if (symbol is not null) - theoryDataTypes.Add(i, symbol); - } + var theoryDataTypes = TypeSymbolFactory.TheoryData_ByGenericArgumentCount(compilation); + var theoryDataRowTypes = TypeSymbolFactory.TheoryDataRow_ByGenericArgumentCount(compilation); context.RegisterSyntaxNodeAction(context => { @@ -174,11 +168,13 @@ memberReturnType is INamedTypeSymbol namedMemberReturnType && } // If the member returns TheoryData, ensure that the types are compatible - // If the member does not return TheoryData, gently suggest to the user that TheoryData is better for type safety + // If the member does not return TheoryData (or IEnumerable>, gently suggest to the user to switch for better type safety if (IsTheoryDataType(memberReturnType, theoryDataTypes, out var theoryReturnType)) - VerifyTheoryDataUsage(semanticModel, context, testMethod, theoryReturnType, memberName, declaredMemberTypeSymbol, attributeSyntax); - else if (IsValidMemberReturnType && !IsTheoryDataRowType(memberReturnType, iEnumerableOfTheoryDataRowType)) - ReportMemberReturnsTypeUnsafeValue(context, attributeSyntax); + VerifyTheoryDataUsage(semanticModel, context, testMethod, theoryDataTypes[0], theoryReturnType, memberName, declaredMemberTypeSymbol, attributeSyntax); + else if (IsGenericTheoryDataRowType(memberReturnType, iEnumerableOfTheoryDataRowType, theoryDataRowTypes, out var theoryDataReturnType)) + VerifyTheoryDataUsage(semanticModel, context, testMethod, theoryDataRowTypes[0], theoryDataReturnType, memberName, declaredMemberTypeSymbol, attributeSyntax); + else if (IsValidMemberReturnType) + ReportMemberReturnsTypeUnsafeValue(context, attributeSyntax, xunitContext.HasV3References ? "TheoryData<> or IEnumerable>" : "TheoryData<>"); // Get the arguments that are to be passed to the method var extraArguments = attributeSyntax.ArgumentList.Arguments.Skip(1).TakeWhile(a => a.NameEquals is null).ToList(); @@ -292,10 +288,37 @@ public static (INamedTypeSymbol? TestClass, ITypeSymbol? MemberClass) GetClassTy return new List { argumentExpression }; } - static bool IsTheoryDataRowType( + static bool IsGenericTheoryDataRowType( ITypeSymbol? memberReturnType, - INamedTypeSymbol? iEnumerableOfTheoryDataRowType) => - iEnumerableOfTheoryDataRowType?.IsAssignableFrom(memberReturnType) ?? false; + INamedTypeSymbol? iEnumerableOfTheoryDataRowType, + Dictionary theoryDataRowTypes, + [NotNullWhen(true)] out INamedTypeSymbol? theoryReturnType) + { + theoryReturnType = default; + + if (iEnumerableOfTheoryDataRowType is null) + return false; + var namedReturnType = UnwrapIEnumerableOfT(memberReturnType, iEnumerableOfTheoryDataRowType.OriginalDefinition); + if (namedReturnType is null) + return false; + + INamedTypeSymbol? working = namedReturnType; + while (working is not null) + { + var returnTypeArguments = working.TypeArguments; + if (returnTypeArguments.Length != 0 + && theoryDataRowTypes.TryGetValue(returnTypeArguments.Length, out var theoryDataType) + && SymbolEqualityComparer.Default.Equals(theoryDataType, working.OriginalDefinition)) + break; + working = working.BaseType; + } + + if (working is null) + return false; + + theoryReturnType = working; + return true; + } static bool IsTheoryDataType( ITypeSymbol? memberReturnType, @@ -407,12 +430,14 @@ static void ReportMemberMethodParameterNullability( static void ReportMemberMethodTheoryDataExtraTypeArguments( SyntaxNodeAnalysisContext context, Location location, - ImmutableDictionary.Builder builder) => + ImmutableDictionary.Builder builder, + INamedTypeSymbol theoryDataType) => context.ReportDiagnostic( Diagnostic.Create( Descriptors.X1038_MemberDataTheoryDataTypeArgumentsMustMatchTestMethodParameters_ExtraTypeParameters, location, - builder.ToImmutable() + builder.ToImmutable(), + SymbolDisplay.ToDisplayString(theoryDataType) ) ); @@ -455,12 +480,14 @@ static void ReportMemberMethodTheoryDataNullability( static void ReportMemberMethodTheoryDataTooFewTypeArguments( SyntaxNodeAnalysisContext context, Location location, - ImmutableDictionary.Builder builder) => + ImmutableDictionary.Builder builder, + INamedTypeSymbol theoryDataType) => context.ReportDiagnostic( Diagnostic.Create( Descriptors.X1037_MemberDataTheoryDataTypeArgumentsMustMatchTestMethodParameters_TooFewTypeParameters, location, - builder.ToImmutable() + builder.ToImmutable(), + SymbolDisplay.ToDisplayString(theoryDataType) ) ); @@ -502,11 +529,13 @@ static void ReportNonPublicPropertyGetter( static void ReportMemberReturnsTypeUnsafeValue( SyntaxNodeAnalysisContext context, - AttributeSyntax attribute) => + AttributeSyntax attribute, + string suggestedAlternative) => context.ReportDiagnostic( Diagnostic.Create( Descriptors.X1042_MemberDataTheoryDataIsRecommendedForStronglyTypedAnalysis, - attribute.GetLocation() + attribute.GetLocation(), + suggestedAlternative ) ); @@ -558,6 +587,24 @@ static void ReportUseNameof( ); } + static INamedTypeSymbol? UnwrapIEnumerableOfT( + ITypeSymbol? type, + INamedTypeSymbol iEnumerableType) + { + if (type is null) + return null; + + IEnumerable interfaces = type.AllInterfaces; + if (type is INamedTypeSymbol namedType) + interfaces = interfaces.Concat([namedType]); + + foreach (var @interface in interfaces) + if (SymbolEqualityComparer.Default.Equals(@interface.OriginalDefinition, iEnumerableType)) + return @interface.TypeArguments[0] as INamedTypeSymbol; + + return null; + } + static void VerifyDataMethodParameterUsage( SemanticModel semanticModel, SyntaxNodeAnalysisContext context, @@ -694,6 +741,7 @@ static void VerifyTheoryDataUsage( SemanticModel semanticModel, SyntaxNodeAnalysisContext context, MethodDeclarationSyntax testMethod, + INamedTypeSymbol theoryDataType, INamedTypeSymbol theoryReturnType, string memberName, ITypeSymbol memberType, @@ -716,7 +764,7 @@ static void VerifyTheoryDataUsage( var builder = ImmutableDictionary.CreateBuilder(); builder[Constants.Properties.MemberName] = memberName; - ReportMemberMethodTheoryDataTooFewTypeArguments(context, attributeSyntax.GetLocation(), builder); + ReportMemberMethodTheoryDataTooFewTypeArguments(context, attributeSyntax.GetLocation(), builder, theoryDataType); return; } @@ -774,7 +822,7 @@ static void VerifyTheoryDataUsage( var builder = ImmutableDictionary.CreateBuilder(); builder[Constants.Properties.MemberName] = memberName; - ReportMemberMethodTheoryDataExtraTypeArguments(context, attributeSyntax.GetLocation(), builder); + ReportMemberMethodTheoryDataExtraTypeArguments(context, attributeSyntax.GetLocation(), builder, theoryDataType); } } } diff --git a/src/xunit.analyzers/X1000/TheoryDataRowArgumentsShouldBeSerializable.cs b/src/xunit.analyzers/X1000/TheoryDataRowArgumentsShouldBeSerializable.cs index 9f66ee22..95265072 100644 --- a/src/xunit.analyzers/X1000/TheoryDataRowArgumentsShouldBeSerializable.cs +++ b/src/xunit.analyzers/X1000/TheoryDataRowArgumentsShouldBeSerializable.cs @@ -23,8 +23,8 @@ public override void AnalyzeCompilation( Guard.ArgumentNotNull(context); Guard.ArgumentNotNull(xunitContext); - var theoryDataRowType = TypeSymbolFactory.TheoryDataRow(context.Compilation); - if (theoryDataRowType is null) + var theoryDataRowTypes = TypeSymbolFactory.TheoryDataRow_ByGenericArgumentCount(context.Compilation); + if (theoryDataRowTypes.Count == 0) return; if (SerializableTypeSymbols.Create(context.Compilation, xunitContext) is not SerializableTypeSymbols typeSymbols) @@ -37,7 +37,11 @@ public override void AnalyzeCompilation( if (context.Operation is not IObjectCreationOperation objectCreation) return; - if (!SymbolEqualityComparer.Default.Equals(theoryDataRowType, objectCreation.Type)) + var creationType = objectCreation.Type as INamedTypeSymbol; + if (creationType is not null && creationType.IsGenericType) + creationType = creationType.OriginalDefinition; + + if (!theoryDataRowTypes.Values.Contains(creationType, SymbolEqualityComparer.Default)) return; var argumentOperations = GetConstructorArguments(objectCreation); @@ -76,6 +80,30 @@ argumentOperation.SemanticModel is null static IReadOnlyList? GetConstructorArguments(IObjectCreationOperation objectCreation) { + // If this is the generic TheoryDataRow, then just return the arguments as-is + if (objectCreation.Type is INamedTypeSymbol creationType && creationType.IsGenericType) + { + var result = new List(); + + for (var idx = 0; idx < objectCreation.Arguments.Length; ++idx) + { +#if ROSLYN_3_11 + var elementValue = objectCreation.Arguments[idx].Children.FirstOrDefault(); +#else + var elementValue = objectCreation.Arguments[idx].ChildOperations.FirstOrDefault(); +#endif + while (elementValue is IConversionOperation conversion) + elementValue = conversion.Operand; + + if (elementValue is not null) + result.Add(elementValue); + } + + return result; + } + + // Non-generic TheoryDataRow, which means we should have a single argument + // which is the params array of values, which we need to unpack. if (objectCreation.Arguments.FirstOrDefault() is not IArgumentOperation argumentOperation) return null;