diff --git a/src/xunit.analyzers.tests/Analyzers/X3000/X3006_TestCaseMustBeSerializableTests.cs b/src/xunit.analyzers.tests/Analyzers/X3000/X3006_TestCaseMustBeSerializableTests.cs new file mode 100644 index 00000000..3370fc90 --- /dev/null +++ b/src/xunit.analyzers.tests/Analyzers/X3000/X3006_TestCaseMustBeSerializableTests.cs @@ -0,0 +1,42 @@ +using System.Threading.Tasks; +using Xunit; +using Verify = CSharpVerifier; + +public class X3006_TestCaseMustBeSerializableTests +{ + static string CS0535( + string interfaceName, + int memberCount) + { + var result = interfaceName; + + while (memberCount-- > 0) + result = $"{{|CS0535:{result}|}}"; + + return result; + } + + [Fact] + public async ValueTask V3_only_NonAOT() + { + var source = $$""" + using Xunit.Sdk; + + [assembly: RegisterXunitSerializer(typeof(MySerializer), typeof(ExternalSerializedTestCase))] + + public class NonTestCase { } + + public abstract class AbstractTestCase : {{CS0535("ITestCase", 19)}} { } + + public sealed class SelfSerializedTestCase : {{CS0535("ITestCase", 19)}}, {{CS0535("IXunitSerializable", 2)}} { } + + public sealed class ExternalSerializedTestCase : {{CS0535("ITestCase", 19)}} { } + + public sealed class {|xUnit3006:UnserializedTestCase|} : {{CS0535("ITestCase", 19)}} { } + + public class MySerializer : {{CS0535("IXunitSerializer", 3)}} { } + """; + + await Verify.VerifyAnalyzerV3NonAot(source); + } +} diff --git a/src/xunit.analyzers.tests/Analyzers/X3000/X3007_TestCaseMustBeSerializableTests.cs b/src/xunit.analyzers.tests/Analyzers/X3000/X3007_TestCaseMustBeSerializableTests.cs new file mode 100644 index 00000000..15da7b4b --- /dev/null +++ b/src/xunit.analyzers.tests/Analyzers/X3000/X3007_TestCaseMustBeSerializableTests.cs @@ -0,0 +1,44 @@ +using System.Threading.Tasks; +using Xunit; +using Verify = CSharpVerifier; + +public class X3007_TestCaseMustBeSerializableTests +{ + static string CS0535( + string interfaceName, + int memberCount) + { + var result = interfaceName; + + while (memberCount-- > 0) + result = $"{{|CS0535:{result}|}}"; + + return result; + } + + [Fact] + public async ValueTask V3_only_NonAOT() + { + var source = $$""" + using Xunit.Sdk; + + [assembly: RegisterXunitSerializer(typeof(MySerializer), typeof(ExternalSerializedTestCase))] + + public class NonTestCase { } + + public abstract class AbstractTestCase : {{CS0535("ITestCase", 19)}} { } + + public class SelfSerializedTestCase : {{CS0535("ITestCase", 19)}}, {{CS0535("IXunitSerializable", 2)}} { } + + public class ExternalSerializedTestCase : {{CS0535("ITestCase", 19)}} { } + + public class DerivedTestCase : ExternalSerializedTestCase { } + + public class {|xUnit3007:UnserializedTestCase|} : {{CS0535("ITestCase", 19)}} { } + + public class MySerializer : {{CS0535("IXunitSerializer", 3)}} { } + """; + + await Verify.VerifyAnalyzerV3NonAot(source); + } +} diff --git a/src/xunit.analyzers/Utility/Descriptors.xUnit3xxx.cs b/src/xunit.analyzers/Utility/Descriptors.xUnit3xxx.cs index 38d9117b..b7c74b5d 100644 --- a/src/xunit.analyzers/Utility/Descriptors.xUnit3xxx.cs +++ b/src/xunit.analyzers/Utility/Descriptors.xUnit3xxx.cs @@ -60,9 +60,23 @@ public static partial class Descriptors "Type '{0}' must have a non-obsolete public constructor: public {0}({1})" ); - // Placeholder for rule X3006 + public static DiagnosticDescriptor X3006_TestCaseImplementationMustBeSerializable { get; } = + Diagnostic( + "xUnit3006", + "Test case implementation must be serializable", + Extensibility, + Error, + "Class '{0}' implements '{1}' but is not serializable. Test cases must be serializable to support test discovery and execution. Implement '{2}' or register an external IXunitSerializer." + ); - // Placeholder for rule X3007 + public static DiagnosticDescriptor X3007_TestCaseImplementationMightNotBeSerializable { get; } = + Diagnostic( + "xUnit3007", + "Test case implementation might not be serializable", + Extensibility, + Warning, + "Class '{0}' implements '{1}' but might not be serializable. Test cases must be serializable to support test discovery and execution. Consider implementing '{2}' or registering an external IXunitSerializer." + ); // Placeholder for rule X3008 diff --git a/src/xunit.analyzers/Utility/SerializabilityAnalyzer.cs b/src/xunit.analyzers/Utility/SerializabilityAnalyzer.cs index 61affdcb..d78b6474 100644 --- a/src/xunit.analyzers/Utility/SerializabilityAnalyzer.cs +++ b/src/xunit.analyzers/Utility/SerializabilityAnalyzer.cs @@ -16,7 +16,7 @@ public sealed class SerializabilityAnalyzer(SerializableTypeSymbols typeSymbols) /// The logic in this method corresponds to the logic in SerializationHelper.IsSerializable /// and SerializationHelper.Serialize. /// - public Serializability AnalayzeSerializability( + public Serializability AnalyzeSerializability( ITypeSymbol type, XunitContext xunitContext) { @@ -28,7 +28,7 @@ public Serializability AnalayzeSerializability( return Serializability.NeverSerializable; if (type.TypeKind == TypeKind.Array && type is IArrayTypeSymbol arrayType) - return AnalayzeSerializability(arrayType.ElementType, xunitContext); + return AnalyzeSerializability(arrayType.ElementType, xunitContext); if (typeSymbols.Type.IsAssignableFrom(type)) return Serializability.AlwaysSerializable; diff --git a/src/xunit.analyzers/X1000/TheoryDataRowArgumentsShouldBeSerializable.cs b/src/xunit.analyzers/X1000/TheoryDataRowArgumentsShouldBeSerializable.cs index eae26704..9bff38b2 100644 --- a/src/xunit.analyzers/X1000/TheoryDataRowArgumentsShouldBeSerializable.cs +++ b/src/xunit.analyzers/X1000/TheoryDataRowArgumentsShouldBeSerializable.cs @@ -56,7 +56,7 @@ public override void AnalyzeCompilation( if (analyzer.TypeShouldBeIgnored(argumentOperation.Type)) continue; - var serializability = analyzer.AnalayzeSerializability(argumentOperation.Type, xunitContext); + var serializability = analyzer.AnalyzeSerializability(argumentOperation.Type, xunitContext); if (serializability != Serializability.AlwaysSerializable) { diff --git a/src/xunit.analyzers/X1000/TheoryDataTypeArgumentsShouldBeSerializable.cs b/src/xunit.analyzers/X1000/TheoryDataTypeArgumentsShouldBeSerializable.cs index ca919529..56ab6ea3 100644 --- a/src/xunit.analyzers/X1000/TheoryDataTypeArgumentsShouldBeSerializable.cs +++ b/src/xunit.analyzers/X1000/TheoryDataTypeArgumentsShouldBeSerializable.cs @@ -68,7 +68,7 @@ public override void AnalyzeCompilation( if (analyzer.TypeShouldBeIgnored(type)) continue; - var serializability = analyzer.AnalayzeSerializability(type, xunitContext); + var serializability = analyzer.AnalyzeSerializability(type, xunitContext); if (serializability != Serializability.AlwaysSerializable) context.ReportDiagnostic( diff --git a/src/xunit.analyzers/X3000/TestCaseMustBeSerializable.cs b/src/xunit.analyzers/X3000/TestCaseMustBeSerializable.cs new file mode 100644 index 00000000..e1a45b53 --- /dev/null +++ b/src/xunit.analyzers/X3000/TestCaseMustBeSerializable.cs @@ -0,0 +1,53 @@ +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace Xunit.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class TestCaseMustBeSerializable() : + XunitV3DiagnosticAnalyzer( + Descriptors.X3006_TestCaseImplementationMustBeSerializable, + Descriptors.X3007_TestCaseImplementationMightNotBeSerializable) +{ + public override void AnalyzeCompilation( + CompilationStartAnalysisContext context, + XunitContext xunitContext) + { + Guard.ArgumentNotNull(context); + Guard.ArgumentNotNull(xunitContext); + + if (SerializableTypeSymbols.Create(context.Compilation, xunitContext) is not SerializableTypeSymbols typeSymbols) + return; + + var iTestCaseType = xunitContext.Common.ITestCaseType; + if (iTestCaseType is null) + return; + + context.RegisterSymbolAction(context => + { + if (context.Symbol is not INamedTypeSymbol namedType) + return; + if (namedType.TypeKind != TypeKind.Class) + return; + if (namedType.IsAbstract) + return; + if (!iTestCaseType.IsAssignableFrom(namedType)) + return; + if (typeSymbols.IXunitSerializable.IsAssignableFrom(namedType) || typeSymbols.TypesWithCustomSerializers.Any(t => t.IsAssignableFrom(namedType))) + return; + + context.ReportDiagnostic( + Diagnostic.Create( + namedType.IsSealed + ? Descriptors.X3006_TestCaseImplementationMustBeSerializable + : Descriptors.X3007_TestCaseImplementationMightNotBeSerializable, + namedType.Locations.First(), + namedType.Name, + iTestCaseType.ToDisplayString(), + xunitContext.Common.IXunitSerializableType?.ToDisplayString() ?? "IXunitSerializable" + ) + ); + }, SymbolKind.NamedType); + } +}