diff --git a/src/xunit.analyzers.tests/Analyzers/X1000/TheoryDataRowArgumentsShouldBeSerializableTests.cs b/src/xunit.analyzers.tests/Analyzers/X1000/TheoryDataRowArgumentsShouldBeSerializableTests.cs index 338d33cf..20d86d01 100644 --- a/src/xunit.analyzers.tests/Analyzers/X1000/TheoryDataRowArgumentsShouldBeSerializableTests.cs +++ b/src/xunit.analyzers.tests/Analyzers/X1000/TheoryDataRowArgumentsShouldBeSerializableTests.cs @@ -119,6 +119,54 @@ public void Serialize(IXunitSerializationInfo info) {{ }} await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp8, source); } + [Theory] + [InlineData("CustomSerialized")] + [InlineData("CustomSerializedDerived")] + public async Task IXunitSerializerValue_DoesNotTrigger(string type) + { + var source = string.Format(/* lang=c#-test */ """ + #nullable enable + + using System; + using System.Collections.Generic; + using Xunit; + using Xunit.Sdk; + + [assembly: RegisterXunitSerializer(typeof(CustomSerializer), typeof(ICustomSerialized))] + + public class MyClass {{ + public IEnumerable MyMethod() {{ + var value = new {0}(); + var defaultValue = default({0}); + var nullValue = default({0}?); + var arrayValue = new {0}[0]; + + yield return new TheoryDataRow(value, defaultValue, nullValue, arrayValue); + yield return new TheoryDataRow<{0}, {0}?, {0}?, {0}[]>(new {0}(), default({0}), default({0}?), new {0}[0]); + }} + }} + + public interface ICustomSerialized {{ }} + + public class CustomSerialized : ICustomSerialized {{ }} + + public class CustomSerializedDerived : CustomSerialized {{ }} + + public class CustomSerializer : IXunitSerializer {{ + public object Deserialize(Type type, string serializedValue) => + throw new NotImplementedException(); + + public bool IsSerializable(Type type, object? value) => + true; + + public string Serialize(object value) => + throw new NotImplementedException(); + }} + """, type); + + await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp8, source); + } + [Theory] [InlineData("Delegate", "Delegate?", "Delegate?")] [InlineData("Func", "Func?", "Func?")] diff --git a/src/xunit.analyzers.tests/Analyzers/X1000/TheoryDataTypeArgumentsShouldBeSerializableTests.cs b/src/xunit.analyzers.tests/Analyzers/X1000/TheoryDataTypeArgumentsShouldBeSerializableTests.cs index 18c3ce75..fdd77a61 100644 --- a/src/xunit.analyzers.tests/Analyzers/X1000/TheoryDataTypeArgumentsShouldBeSerializableTests.cs +++ b/src/xunit.analyzers.tests/Analyzers/X1000/TheoryDataTypeArgumentsShouldBeSerializableTests.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CSharp; using Xunit; using Verify = CSharpVerifier; @@ -253,6 +254,52 @@ public void Serialize(IXunitSerializationInfo info) {{ }} """, member, attribute, type, ns); } + [Theory] + [MemberData(nameof(TheoryDataMembers), "ICustomSerialized")] + [MemberData(nameof(TheoryDataMembers), "CustomSerialized")] + [MemberData(nameof(TheoryDataMembers), "CustomSerializedDerived")] + public async Task GivenTheory_WithIXunitSerializerTheoryDataMember_DoesNotTrigger( + string member, + string attribute, + string type) + { + var source = string.Format(/* lang=c#-test */ """ + using System; + using Xunit; + using Xunit.Sdk; + + [assembly: RegisterXunitSerializer(typeof(CustomSerializer), typeof(ICustomSerialized))] + + public class TestClass {{ + {0} + + [Theory] + [{1}] + public void TestMethod({2} parameter) {{ }} + }} + + public interface ICustomSerialized {{ }} + + public class CustomSerialized : ICustomSerialized {{ }} + + public class CustomSerializedDerived : CustomSerialized {{ }} + + public class CustomSerializer : IXunitSerializer {{ + public object Deserialize(Type type, string serializedValue) => + throw new NotImplementedException(); + + public bool IsSerializable(Type type, object? value) => + true; + + public string Serialize(object value) => + throw new NotImplementedException(); + }} + """, member, attribute, type + ); + + await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp8, source); + } + [Theory] [MemberData(nameof(TheoryDataMembers), "Delegate")] [MemberData(nameof(TheoryDataMembers), "Delegate[]")] diff --git a/src/xunit.analyzers/Utility/Constants.cs b/src/xunit.analyzers/Utility/Constants.cs index 7c9811b2..9741f5fd 100644 --- a/src/xunit.analyzers/Utility/Constants.cs +++ b/src/xunit.analyzers/Utility/Constants.cs @@ -178,6 +178,7 @@ public static class Xunit public const string LongLivedMarshalByRefObject_Execution_V2 = "Xunit.LongLivedMarshalByRefObject"; public const string LongLivedMarshalByRefObject_RunnerUtility = "Xunit.Sdk.LongLivedMarshalByRefObject"; public const string MemberDataAttribute = "Xunit.MemberDataAttribute"; + public const string RegisterXunitSerializerAttribute_V3 = "Xunit.Sdk.RegisterXunitSerializerAttribute"; public const string TestContext_V3 = "Xunit.TestContext"; public const string TheoryAttribute = "Xunit.TheoryAttribute"; public const string TheoryData = "Xunit.TheoryData"; diff --git a/src/xunit.analyzers/Utility/SerializabilityAnalyzer.cs b/src/xunit.analyzers/Utility/SerializabilityAnalyzer.cs index 50666b4b..b2d39c0b 100644 --- a/src/xunit.analyzers/Utility/SerializabilityAnalyzer.cs +++ b/src/xunit.analyzers/Utility/SerializabilityAnalyzer.cs @@ -47,6 +47,9 @@ public Serializability AnalayzeSerializability(ITypeSymbol type) || type.Equals(typeSymbols.TimeOnly, SymbolEqualityComparer.Default)) return Serializability.AlwaysSerializable; + if (typeSymbols.TypesWithCustomSerializers.Any(t => t.IsAssignableFrom(type))) + return Serializability.AlwaysSerializable; + if (type.TypeKind == TypeKind.Class && !type.IsSealed) return Serializability.PossiblySerializable; diff --git a/src/xunit.analyzers/Utility/SerializableTypeSymbols.cs b/src/xunit.analyzers/Utility/SerializableTypeSymbols.cs index ed57f44b..e10857f8 100644 --- a/src/xunit.analyzers/Utility/SerializableTypeSymbols.cs +++ b/src/xunit.analyzers/Utility/SerializableTypeSymbols.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; using Microsoft.CodeAnalysis; namespace Xunit.Analyzers; @@ -16,6 +18,7 @@ public sealed class SerializableTypeSymbols readonly Lazy timeSpan; readonly Lazy traitDictionary; readonly Lazy type; + readonly Lazy> typesWithCustomSerializers; SerializableTypeSymbols( Compilation compilation, @@ -40,6 +43,21 @@ public sealed class SerializableTypeSymbols timeSpan = new(() => TypeSymbolFactory.TimeSpan(compilation)); traitDictionary = new(() => GetTraitDictionary(compilation)); type = new(() => TypeSymbolFactory.Type(compilation)); + typesWithCustomSerializers = new(() => + { + var registerXunitSerializer = TypeSymbolFactory.RegisterXunitSerializerAttribute_V3(compilation); + if (registerXunitSerializer is null) + return ImmutableArray.Empty; + + return + compilation + .Assembly + .GetAttributes() + .Where(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, registerXunitSerializer) && a.ConstructorArguments.Length > 1 && a.ConstructorArguments[1].Kind == TypedConstantKind.Array) + .SelectMany(a => a.ConstructorArguments[1].Values.Select(v => v.Value as INamedTypeSymbol)) + .WhereNotNull() + .ToImmutableArray(); + }); ClassDataAttribute = classDataAttribute; DataAttribute = dataAttribute; @@ -60,6 +78,7 @@ public sealed class SerializableTypeSymbols public INamedTypeSymbol? TimeSpan => timeSpan.Value; public INamedTypeSymbol? TraitDictionary => traitDictionary.Value; public INamedTypeSymbol? Type => type.Value; + public ImmutableArray TypesWithCustomSerializers => typesWithCustomSerializers.Value; public static SerializableTypeSymbols? Create( Compilation compilation, diff --git a/src/xunit.analyzers/Utility/TypeSymbolFactory.cs b/src/xunit.analyzers/Utility/TypeSymbolFactory.cs index c75f4119..22587c39 100644 --- a/src/xunit.analyzers/Utility/TypeSymbolFactory.cs +++ b/src/xunit.analyzers/Utility/TypeSymbolFactory.cs @@ -286,6 +286,9 @@ public static IArrayTypeSymbol ObjectArray(Compilation compilation) => public static INamedTypeSymbol? OptionalAttribute(Compilation compilation) => Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("System.Runtime.InteropServices.OptionalAttribute"); + public static INamedTypeSymbol? RegisterXunitSerializerAttribute_V3(Compilation compilation) => + Guard.ArgumentNotNull(compilation).GetTypeByMetadataName(Constants.Types.Xunit.RegisterXunitSerializerAttribute_V3); + public static INamedTypeSymbol? SortedSetOfT(Compilation compilation) => Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("System.Collections.Generic.SortedSet`1");