From 31df0bd9e676fbbcd56e33bf5d62850e3d8f2529 Mon Sep 17 00:00:00 2001 From: Vogel612 Date: Sat, 13 Sep 2025 12:28:35 +0200 Subject: [PATCH 1/5] xunit/xunit#1258 Analyzer for uninitialized member data --- ...mberDataShouldReferenceValidMemberTests.cs | 65 +++++++++++++++++++ ...ldReferenceValidMember_NameOfFixerTests.cs | 4 +- .../Utility/Descriptors.xUnit1xxx.cs | 9 ++- .../MemberDataShouldReferenceValidMember.cs | 54 ++++++++++++++- 4 files changed, 128 insertions(+), 4 deletions(-) diff --git a/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs b/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs index 4d8c228d..3bfdb329 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,61 @@ 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; + + [Theory] + [MemberData(nameof(Field))] + [MemberData(nameof(Property))] + [MemberData(nameof(PropertyWithGetBody))] + [MemberData(nameof(PropertyWithGetExpression))] + 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..db64f8a2 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, compilation, context)) + ReportMemberMustBeWrittenTo(context, memberSymbol); + // Unwrap Task or ValueTask, but only for v3 if (xunitContext.HasV3References && memberReturnType is INamedTypeSymbol namedMemberReturnType && @@ -195,6 +199,43 @@ memberReturnType is INamedTypeSymbol namedMemberReturnType && }, SyntaxKind.MethodDeclaration); } + static bool IsInitialized( + ISymbol memberSymbol, + Compilation compilation, + 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 = declarationReference.SyntaxTree.GetCompilationUnitRoot(); + var staticConstructors = declarationContainer.DescendantNodes() + .OfType() + .Where(mds => semantics.GetDeclaredSymbol(mds)?.MethodKind == MethodKind.StaticConstructor); + + foreach (var ctor in staticConstructors) + { + var analysis = semantics.AnalyzeDataFlow(ctor); + if (analysis.WrittenInside.Contains(memberSymbol)) + return true; + } + return false; + } + static ISymbol? FindMemberSymbol( string memberName, ITypeSymbol? type, @@ -528,6 +569,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, From 83fab5c3da75ca16c7d73ad2c4ee19e32b9ce8b2 Mon Sep 17 00:00:00 2001 From: Vogel612 Date: Sat, 13 Sep 2025 14:08:25 +0200 Subject: [PATCH 2/5] Correct discovery of static constructors for memberdata assignment analysis --- ...mberDataShouldReferenceValidMemberTests.cs | 10 +++++++++ .../MemberDataShouldReferenceValidMember.cs | 21 ++++++++++++------- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs b/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs index 3bfdb329..61ce0960 100644 --- a/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs +++ b/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs @@ -1528,12 +1528,22 @@ public class TestClass { 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 _) {} } """; diff --git a/src/xunit.analyzers/X1000/MemberDataShouldReferenceValidMember.cs b/src/xunit.analyzers/X1000/MemberDataShouldReferenceValidMember.cs index db64f8a2..044dfde3 100644 --- a/src/xunit.analyzers/X1000/MemberDataShouldReferenceValidMember.cs +++ b/src/xunit.analyzers/X1000/MemberDataShouldReferenceValidMember.cs @@ -138,7 +138,7 @@ public override void AnalyzeCompilation( if (!memberSymbol.IsStatic) ReportNonStatic(context, attributeSyntax, memberProperties); - if (!IsInitialized(memberSymbol, compilation, context)) + if (!IsInitialized(memberSymbol, context)) ReportMemberMustBeWrittenTo(context, memberSymbol); // Unwrap Task or ValueTask, but only for v3 @@ -201,7 +201,6 @@ memberReturnType is INamedTypeSymbol namedMemberReturnType && static bool IsInitialized( ISymbol memberSymbol, - Compilation compilation, SyntaxNodeAnalysisContext context ) { @@ -222,15 +221,23 @@ SyntaxNodeAnalysisContext context && field.Initializer != null) return true; - var declarationContainer = declarationReference.SyntaxTree.GetCompilationUnitRoot(); + var declarationContainer = declarationSyntax.FirstAncestorOrSelf()!; var staticConstructors = declarationContainer.DescendantNodes() - .OfType() - .Where(mds => semantics.GetDeclaredSymbol(mds)?.MethodKind == MethodKind.StaticConstructor); + .OfType() + .Where(ctor => ctor.Modifiers.Any(SyntaxKind.StaticKeyword)); foreach (var ctor in staticConstructors) { - var analysis = semantics.AnalyzeDataFlow(ctor); - if (analysis.WrittenInside.Contains(memberSymbol)) + // 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; From 0cfa220dbca7e78984888b95f38637d22d6ca8b8 Mon Sep 17 00:00:00 2001 From: Vogel612 Date: Sat, 4 Oct 2025 14:55:35 +0200 Subject: [PATCH 3/5] Outline limitation of static analysis discovery in a test case --- ...mberDataShouldReferenceValidMemberTests.cs | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs b/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs index 61ce0960..c875fecc 100644 --- a/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs +++ b/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs @@ -1533,7 +1533,7 @@ public class TestClass { static TestClass() { - FieldWrittenInStaticConstructor = null; + TestClass.FieldWrittenInStaticConstructor = null; PropertyWrittenInStaticConstructor = null; } @@ -1563,12 +1563,13 @@ public class TestClass { public TestClass() { - Field = null; + TestClass.Field = null; Property = null; } [Theory] - [MemberData(nameof(Field)), MemberData(nameof(Property))] + [MemberData(nameof(Field))] + [MemberData(nameof(Property))] public void TestCase(int _) {} } """; @@ -1580,5 +1581,42 @@ public void TestCase(int _) {} await Verify.VerifyAnalyzer(source, expected); } + + [Fact] + public async ValueTask OutOfScopeCase_GeneratesResult() + { + var source = /* lang=c#-test */ """ + using Xunit; + + public class TheoryInitializer { + static TheoryInitializer() + { + TestClass.Field = null; + TestClass.Property = null; + } + } + + public class TestClass { + public static TheoryData {|#0:Field|}; + public static TheoryData {|#1:Property|} { get; set; } + + static TestClass() + { + new TheoryInitializer(); + } + + [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); + } } } From b29ce817d88c90b4f22067fd4d7e4904bfa908c2 Mon Sep 17 00:00:00 2001 From: Brad Wilson Date: Sun, 12 Oct 2025 11:13:45 -0700 Subject: [PATCH 4/5] Formatting trivia --- ...mberDataShouldReferenceValidMemberTests.cs | 6 +++ .../MemberDataShouldReferenceValidMember.cs | 53 ++++++++++--------- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs b/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs index c875fecc..e2350247 100644 --- a/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs +++ b/src/xunit.analyzers.tests/Analyzers/X1000/MemberDataShouldReferenceValidMemberTests.cs @@ -12,6 +12,7 @@ public async ValueTask V2_and_V3() { var source = /* lang=c#-test */ """ #pragma warning disable xUnit1053 + using System.Collections.Generic; using Xunit; @@ -151,6 +152,7 @@ public async ValueTask V2_and_V3() { var source = /* lang=c#-test */ """ #pragma warning disable xUnit1053 + using System; using System.Collections.Generic; using Xunit; @@ -376,6 +378,7 @@ public async ValueTask V2_and_V3() { var source = /* lang=c#-test */ """ #pragma warning disable xUnit1053 + using Xunit; public class TestClass { @@ -405,6 +408,7 @@ public async ValueTask V2_and_V3() { var source = /* lang=c#-test */ """ #pragma warning disable xUnit1053 + using Xunit; public class TestClassBase { @@ -1368,6 +1372,7 @@ public async ValueTask V2_and_V3() { var source = /* lang=c#-test */ """ #pragma warning disable xUnit1053 + using System.Collections.Generic; using Xunit; @@ -1417,6 +1422,7 @@ public async Task V3_only() { var source = /* lang=c#-test */ """ #pragma warning disable xUnit1053 + using System.Collections.Generic; using System.Threading.Tasks; using Xunit; diff --git a/src/xunit.analyzers/X1000/MemberDataShouldReferenceValidMember.cs b/src/xunit.analyzers/X1000/MemberDataShouldReferenceValidMember.cs index 044dfde3..4418f731 100644 --- a/src/xunit.analyzers/X1000/MemberDataShouldReferenceValidMember.cs +++ b/src/xunit.analyzers/X1000/MemberDataShouldReferenceValidMember.cs @@ -201,8 +201,7 @@ memberReturnType is INamedTypeSymbol namedMemberReturnType && static bool IsInitialized( ISymbol memberSymbol, - SyntaxNodeAnalysisContext context - ) + SyntaxNodeAnalysisContext context) { if (!memberSymbol.IsStatic || memberSymbol is IMethodSymbol) // assume initialized, if nonstatic or method to avoid spurious results @@ -217,29 +216,33 @@ SyntaxNodeAnalysisContext context || prop.ExpressionBody != null)) return true; - if (declarationSyntax is VariableDeclaratorSyntax field - && field.Initializer != null) + 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)); + 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); - }); + 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; } @@ -562,6 +565,17 @@ static void ReportMemberMethodTheoryDataTooFewTypeArguments( ) ); + static void ReportMemberMustBeWrittenTo( + SyntaxNodeAnalysisContext context, + ISymbol memberSymbol) => + context.ReportDiagnostic( + Diagnostic.Create( + Descriptors.X1053_MemberDataMemberMustBeStaticallyWrittenTo, + memberSymbol.Locations.First(), + memberSymbol.Name + ) + ); + static void ReportMissingMember( SyntaxNodeAnalysisContext context, AttributeSyntax attribute, @@ -576,17 +590,6 @@ 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, From 12778283bb6b39ca746456c0f5b8f41bc72767d6 Mon Sep 17 00:00:00 2001 From: Brad Wilson Date: Sun, 12 Oct 2025 11:20:17 -0700 Subject: [PATCH 5/5] Add resolution text to xUnit1053 descriptor --- src/xunit.analyzers/Utility/Descriptors.xUnit1xxx.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/xunit.analyzers/Utility/Descriptors.xUnit1xxx.cs b/src/xunit.analyzers/Utility/Descriptors.xUnit1xxx.cs index cec4d269..f0f6adf9 100644 --- a/src/xunit.analyzers/Utility/Descriptors.xUnit1xxx.cs +++ b/src/xunit.analyzers/Utility/Descriptors.xUnit1xxx.cs @@ -489,7 +489,7 @@ public static partial class Descriptors "The static member used as theory data must be statically initialized.", Usage, Warning, - "The member {0} referenced by MemberData is not initialized before use." + "The member {0} referenced by MemberData is not initialized before use. Add an inline initializer or initialize the value in the static constructor." ); // Placeholder for rule X1054