Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Threading.Tasks;
using Xunit;
using Verify = CSharpVerifier<Xunit.Analyzers.TestCaseMustBeSerializable>;

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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System.Threading.Tasks;
using Xunit;
using Verify = CSharpVerifier<Xunit.Analyzers.TestCaseMustBeSerializable>;

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);
}
}
18 changes: 16 additions & 2 deletions src/xunit.analyzers/Utility/Descriptors.xUnit3xxx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions src/xunit.analyzers/Utility/SerializabilityAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </remarks>
public Serializability AnalayzeSerializability(
public Serializability AnalyzeSerializability(
ITypeSymbol type,
XunitContext xunitContext)
{
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
53 changes: 53 additions & 0 deletions src/xunit.analyzers/X3000/TestCaseMustBeSerializable.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading