diff --git a/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs b/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs index 4d8c228d..61ce0960 100644 --- a/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs +++ b/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs @@ -11,6 +11,7 @@ public class X1014_MemberDataShouldUseNameOfOperator public async ValueTask V2_and_V3() { var source = /* lang=c#-test */ """ + #pragma warning disable xUnit1053 using System.Collections.Generic; using Xunit; @@ -149,6 +150,7 @@ public class X1018_MemberDataMustReferenceValidMemberKind public async ValueTask V2_and_V3() { var source = /* lang=c#-test */ """ + #pragma warning disable xUnit1053 using System; using System.Collections.Generic; using Xunit; @@ -196,6 +198,7 @@ public async ValueTask V2_and_V3() { var source = /* lang=c#-test */ """ #pragma warning disable xUnit1042 + #pragma warning disable xUnit1053 using System; using System.Collections.Generic; @@ -319,6 +322,7 @@ public async ValueTask V3_only() { var source = /* lang=c#-test */ """ #pragma warning disable xUnit1042 + #pragma warning disable xUnit1053 using System.Collections.Generic; using System.Threading.Tasks; @@ -371,6 +375,7 @@ public class X1020_MemberDataPropertyMustHaveGetter public async ValueTask V2_and_V3() { var source = /* lang=c#-test */ """ + #pragma warning disable xUnit1053 using Xunit; public class TestClass { @@ -399,6 +404,7 @@ public class X1021_MemberDataNonMethodShouldNotHaveParameters public async ValueTask V2_and_V3() { var source = /* lang=c#-test */ """ + #pragma warning disable xUnit1053 using Xunit; public class TestClassBase { @@ -1361,6 +1367,7 @@ public class X1042_MemberDataTheoryDataIsRecommendedForStronglyTypedAnalysis public async ValueTask V2_and_V3() { var source = /* lang=c#-test */ """ + #pragma warning disable xUnit1053 using System.Collections.Generic; using Xunit; @@ -1409,6 +1416,7 @@ public void TestMethod2(int _) { } public async Task V3_only() { var source = /* lang=c#-test */ """ + #pragma warning disable xUnit1053 using System.Collections.Generic; using System.Threading.Tasks; using Xunit; @@ -1506,4 +1514,71 @@ public void TestMethod6(int _) { } await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp7_1, source, expected); } } + + public class X1053_MemberDataMemberMustBeStaticallyWrittenTo + { + [Fact] + public async ValueTask Initializers_AndGetExpressions_MarkAsWrittenTo() + { + var source = /* lang=c#-test */ """ + using Xunit; + + public class TestClass { + public static TheoryData Field = null; + public static TheoryData Property { get; } = null; + public static TheoryData PropertyWithGetBody { get { return null; } } + public static TheoryData PropertyWithGetExpression => null; + public static TheoryData FieldWrittenInStaticConstructor; + public static TheoryData PropertyWrittenInStaticConstructor { get; set; } + + static TestClass() + { + FieldWrittenInStaticConstructor = null; + PropertyWrittenInStaticConstructor = null; + } + + [Theory] + [MemberData(nameof(Field))] + [MemberData(nameof(Property))] + [MemberData(nameof(PropertyWithGetBody))] + [MemberData(nameof(PropertyWithGetExpression))] + [MemberData(nameof(FieldWrittenInStaticConstructor))] + [MemberData(nameof(PropertyWrittenInStaticConstructor))] + public void TestCase(int _) {} + } + """; + + await Verify.VerifyAnalyzer(source, []); + } + + [Fact] + public async ValueTask SimpleCase_GeneratesResult() + { + var source = /* lang=c#-test */ """ + using Xunit; + + public class TestClass { + public static TheoryData {|#0:Field|}; + public static TheoryData {|#1:Property|} { get; set; } + + public TestClass() + { + Field = null; + Property = null; + } + + [Theory] + [MemberData(nameof(Field)), MemberData(nameof(Property))] + public void TestCase(int _) {} + } + """; + + var expected = new[] { + Verify.Diagnostic("xUnit1053").WithLocation(0).WithArguments("Field"), + Verify.Diagnostic("xUnit1053").WithLocation(1).WithArguments("Property"), + }; + + await Verify.VerifyAnalyzer(source, expected); + } + } } diff --git a/src/xunit.analyzers.tests/Fixes/X1000/MemberDataShouldReferenceValidMember_NameOfFixerTests.cs b/src/xunit.analyzers.tests/Fixes/X1000/MemberDataShouldReferenceValidMember_NameOfFixerTests.cs index 0ef17674..255b34ac 100644 --- a/src/xunit.analyzers.tests/Fixes/X1000/MemberDataShouldReferenceValidMember_NameOfFixerTests.cs +++ b/src/xunit.analyzers.tests/Fixes/X1000/MemberDataShouldReferenceValidMember_NameOfFixerTests.cs @@ -14,7 +14,7 @@ public async Task ConvertStringToNameOf() using Xunit; public class TestClass { - public static TheoryData DataSource; + public static TheoryData DataSource = new TheoryData(); [Theory] [MemberData({|xUnit1014:"DataSource"|})] @@ -27,7 +27,7 @@ public void TestMethod(int a) { } using Xunit; public class TestClass { - public static TheoryData DataSource; + public static TheoryData DataSource = new TheoryData(); [Theory] [MemberData(nameof(DataSource))] diff --git a/src/xunit.analyzers/Utility/Descriptors.xUnit1xxx.cs b/src/xunit.analyzers/Utility/Descriptors.xUnit1xxx.cs index d2c0416e..cec4d269 100644 --- a/src/xunit.analyzers/Utility/Descriptors.xUnit1xxx.cs +++ b/src/xunit.analyzers/Utility/Descriptors.xUnit1xxx.cs @@ -483,7 +483,14 @@ public static partial class Descriptors "'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." ); - // Placeholder for rule X1053 + public static DiagnosticDescriptor X1053_MemberDataMemberMustBeStaticallyWrittenTo { get; } = + Diagnostic( + "xUnit1053", + "The static member used as theory data must be statically initialized.", + Usage, + Warning, + "The member {0} referenced by MemberData is not initialized before use." + ); // Placeholder for rule X1054 diff --git a/src/xunit.analyzers/X1000/MemberDataShouldReferenceValidMember.cs b/src/xunit.analyzers/X1000/MemberDataShouldReferenceValidMember.cs index 722eb3d1..044dfde3 100644 --- a/src/xunit.analyzers/X1000/MemberDataShouldReferenceValidMember.cs +++ b/src/xunit.analyzers/X1000/MemberDataShouldReferenceValidMember.cs @@ -33,7 +33,8 @@ public MemberDataShouldReferenceValidMember() : Descriptors.X1038_TheoryDataTypeArgumentsMustMatchTestMethodParameters_ExtraTypeParameters, Descriptors.X1039_TheoryDataTypeArgumentsMustMatchTestMethodParameters_IncompatibleTypes, Descriptors.X1040_TheoryDataTypeArgumentsMustMatchTestMethodParameters_IncompatibleNullability, - Descriptors.X1042_MemberDataTheoryDataIsRecommendedForStronglyTypedAnalysis + Descriptors.X1042_MemberDataTheoryDataIsRecommendedForStronglyTypedAnalysis, + Descriptors.X1053_MemberDataMemberMustBeStaticallyWrittenTo ) { } @@ -137,6 +138,9 @@ public override void AnalyzeCompilation( if (!memberSymbol.IsStatic) ReportNonStatic(context, attributeSyntax, memberProperties); + if (!IsInitialized(memberSymbol, context)) + ReportMemberMustBeWrittenTo(context, memberSymbol); + // Unwrap Task or ValueTask, but only for v3 if (xunitContext.HasV3References && memberReturnType is INamedTypeSymbol namedMemberReturnType && @@ -195,6 +199,50 @@ memberReturnType is INamedTypeSymbol namedMemberReturnType && }, SyntaxKind.MethodDeclaration); } + static bool IsInitialized( + ISymbol memberSymbol, + SyntaxNodeAnalysisContext context + ) + { + if (!memberSymbol.IsStatic || memberSymbol is IMethodSymbol) + // assume initialized, if nonstatic or method to avoid spurious results + return true; + + var semantics = context.SemanticModel; + var declarationReference = memberSymbol.DeclaringSyntaxReferences.First(); + var declarationSyntax = declarationReference.GetSyntax(); + if (declarationSyntax is PropertyDeclarationSyntax prop + && (prop.Initializer != null + || prop.AccessorList?.Accessors.FirstOrDefault(decl => decl.Kind() == SyntaxKind.GetAccessorDeclaration)?.Body != null + || prop.ExpressionBody != null)) + return true; + + if (declarationSyntax is VariableDeclaratorSyntax field + && field.Initializer != null) + return true; + + var declarationContainer = declarationSyntax.FirstAncestorOrSelf()!; + var staticConstructors = declarationContainer.DescendantNodes() + .OfType() + .Where(ctor => ctor.Modifiers.Any(SyntaxKind.StaticKeyword)); + + foreach (var ctor in staticConstructors) + { + // Look for direct assignments to the member + var assignments = ctor.DescendantNodes(descendIntoChildren: _ => true, descendIntoTrivia: false) + .OfType() + .Where(assignment => + { + var assignedSymbol = semantics.GetSymbolInfo(assignment.Left).Symbol; + return SymbolEqualityComparer.Default.Equals(assignedSymbol?.OriginalDefinition, memberSymbol); + }); + + if (assignments.Any()) + return true; + } + return false; + } + static ISymbol? FindMemberSymbol( string memberName, ITypeSymbol? type, @@ -528,6 +576,17 @@ static void ReportMissingMember( ) ); + static void ReportMemberMustBeWrittenTo( + SyntaxNodeAnalysisContext context, + ISymbol memberSymbol) => + context.ReportDiagnostic( + Diagnostic.Create( + Descriptors.X1053_MemberDataMemberMustBeStaticallyWrittenTo, + memberSymbol.Locations.First(), + memberSymbol.Name + ) + ); + static void ReportNonPublicAccessibility( SyntaxNodeAnalysisContext context, AttributeSyntax attribute,