Skip to content

Commit

Permalink
Update xUnit1045 and xUnit1047 to not trigger with IFormattable and I…
Browse files Browse the repository at this point in the history
…Parsable<TSelf>
  • Loading branch information
bradwilson committed Feb 7, 2025
1 parent e6c18a1 commit c485083
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,103 @@ public class PossiblySerializableUnsealedClass {{ }}
await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp8, source, expected);
}

[Fact]
public async Task IFormattableAndIParseable_DoesNotTrigger()
{
var source = /* lang=c#-test */ """
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using Xunit;
public class Formattable : IFormattable
{
public string ToString(string? format, IFormatProvider? formatProvider) => string.Empty;
}
public class Parsable : IParsable<Parsable>
{
public static Parsable Parse(string s, IFormatProvider? provider) => new();
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out Parsable result)
{
result = new();
return true;
}
}
public class FormattableAndParsable : IFormattable, IParsable<FormattableAndParsable>
{
public static FormattableAndParsable Parse(string s, IFormatProvider? provider) => new();
public string ToString(string? format, IFormatProvider? formatProvider) => string.Empty;
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out FormattableAndParsable result)
{
result = new();
return true;
}
}
public class FormattableData {
public IEnumerable<TheoryDataRowBase> MyMethod() {
var defaultValue = default(Formattable);
var nullValue = default(Formattable?);
var arrayValue = new Formattable[0];
yield return new TheoryDataRow({|#0:defaultValue|}, {|#1:nullValue|}, {|#2:arrayValue|});
yield return new TheoryDataRow<Formattable, Formattable, Formattable[]>({|#3:default(Formattable)|}, {|#4:default(Formattable?)|}, {|#5:new Formattable[0]|});
}
}
public class ParsableData {
public IEnumerable<TheoryDataRowBase> MyMethod() {
var defaultValue = default(Parsable);
var nullValue = default(Parsable?);
var arrayValue = new Parsable[0];
yield return new TheoryDataRow({|#10:defaultValue|}, {|#11:nullValue|}, {|#12:arrayValue|});
yield return new TheoryDataRow<Parsable, Parsable, Parsable[]>({|#13:default(Parsable)|}, {|#14:default(Parsable?)|}, {|#15:new Parsable[0]|});
}
}
public class FormattableAndParsableData {
public IEnumerable<TheoryDataRowBase> MyMethod() {
var defaultValue = default(FormattableAndParsable);
var nullValue = default(FormattableAndParsable?);
var arrayValue = new FormattableAndParsable[0];
yield return new TheoryDataRow(defaultValue, nullValue, arrayValue);
yield return new TheoryDataRow<FormattableAndParsable, FormattableAndParsable, FormattableAndParsable[]>(default(FormattableAndParsable), default(FormattableAndParsable?), new FormattableAndParsable[0]);
}
}
""";
#if ROSLYN_LATEST && NET8_0_OR_GREATER
var expected = new[] {
Verify.Diagnostic("xUnit1047").WithLocation(0).WithArguments("defaultValue", "Formattable?"),
Verify.Diagnostic("xUnit1047").WithLocation(1).WithArguments("nullValue", "Formattable?"),
Verify.Diagnostic("xUnit1047").WithLocation(2).WithArguments("arrayValue", "Formattable[]"),
Verify.Diagnostic("xUnit1047").WithLocation(3).WithArguments("default(Formattable)", "Formattable?"),
Verify.Diagnostic("xUnit1047").WithLocation(4).WithArguments("default(Formattable?)", "Formattable?"),
Verify.Diagnostic("xUnit1047").WithLocation(5).WithArguments("new Formattable[0]", "Formattable[]"),

Verify.Diagnostic("xUnit1047").WithLocation(10).WithArguments("defaultValue", "Parsable?"),
Verify.Diagnostic("xUnit1047").WithLocation(11).WithArguments("nullValue", "Parsable?"),
Verify.Diagnostic("xUnit1047").WithLocation(12).WithArguments("arrayValue", "Parsable[]"),
Verify.Diagnostic("xUnit1047").WithLocation(13).WithArguments("default(Parsable)", "Parsable?"),
Verify.Diagnostic("xUnit1047").WithLocation(14).WithArguments("default(Parsable?)", "Parsable?"),
Verify.Diagnostic("xUnit1047").WithLocation(15).WithArguments("new Parsable[0]", "Parsable[]"),
};

await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp11, source, expected);
#else
// For some reason, 'dotnet format' complains about the indenting of #nullable enable in the source code line
// above if the #if statement surrounds the whole method, so we use this "workaround" to do nothing in that case.
Assert.NotEqual(string.Empty, source);
await Task.Yield();
#endif
}


[Theory]
[InlineData("object")]
[InlineData("Dictionary<int, string>")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,76 @@ public void TestMethod({2} parameter) {{ }}
await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp9, source);
}

[Fact]
public async Task IFormattableAndIParseable_TriggersInV2_DoesNotTriggerInV3()
{
var source = /* lang=c#-test */ """
#nullable enable
using System;
using System.Diagnostics.CodeAnalysis;
using Xunit;
public class Formattable : IFormattable
{
public string ToString(string? format, IFormatProvider? formatProvider) => string.Empty;
}
public class Parsable : IParsable<Parsable>
{
public static Parsable Parse(string s, IFormatProvider? provider) => new();
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out Parsable result)
{
result = new();
return true;
}
}
public class FormattableAndParsable : IFormattable, IParsable<FormattableAndParsable>
{
public static FormattableAndParsable Parse(string s, IFormatProvider? provider) => new();
public string ToString(string? format, IFormatProvider? formatProvider) => string.Empty;
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out FormattableAndParsable result)
{
result = new();
return true;
}
}
public class TestClass {
public static readonly TheoryData<Formattable> FormattableData = new TheoryData<Formattable>() { };
public static readonly TheoryData<Parsable> ParsableData = new TheoryData<Parsable>() { };
public static readonly TheoryData<FormattableAndParsable> FormattableAndParsableData = new TheoryData<FormattableAndParsable>() { };
[Theory]
[{|#0:MemberData(nameof(FormattableData))|}]
[{|#1:MemberData(nameof(ParsableData))|}]
[{|#2:MemberData(nameof(FormattableAndParsableData))|}]
public void TestMethod(object parameter) { }
}
""";
#if ROSLYN_LATEST && NET8_0_OR_GREATER
var expectedV2 = new[] {
Verify.Diagnostic("xUnit1045").WithLocation(0).WithArguments("Formattable"),
Verify.Diagnostic("xUnit1045").WithLocation(1).WithArguments("Parsable"),
Verify.Diagnostic("xUnit1045").WithLocation(2).WithArguments("FormattableAndParsable"),
};
var expectedV3 = new[] {
Verify.Diagnostic("xUnit1045").WithLocation(0).WithArguments("Formattable"),
Verify.Diagnostic("xUnit1045").WithLocation(1).WithArguments("Parsable"),
};

await Verify.VerifyAnalyzerV2(LanguageVersion.CSharp11, source, expectedV2);
await Verify.VerifyAnalyzerV3(LanguageVersion.CSharp11, source, expectedV3);
#else
// For some reason, 'dotnet format' complains about the indenting of #nullable enable in the source code line
// above if the #if statement surrounds the whole method, so we use this "workaround" to do nothing in that case.
Assert.NotEqual(string.Empty, source);
await Task.Yield();
#endif
}


[Theory]
[MemberData(nameof(TheoryDataMembers), "object")]
[MemberData(nameof(TheoryDataMembers), "object[]")]
Expand Down
7 changes: 7 additions & 0 deletions src/xunit.analyzers/Utility/SerializabilityAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ public Serializability AnalayzeSerializability(
|| type.Equals(typeSymbols.Uri, SymbolEqualityComparer.Default)
|| type.Equals(typeSymbols.Version, SymbolEqualityComparer.Default))
return Serializability.AlwaysSerializable;

if (typeSymbols.IFormattable.IsAssignableFrom(type) && typeSymbols.IParsableOfT is not null)
{
var iParsableOfSelf = typeSymbols.IParsableOfT.Construct(type);
if (iParsableOfSelf.IsAssignableFrom(type))
return Serializability.AlwaysSerializable;
}
}

if (typeSymbols.TypesWithCustomSerializers.Any(t => t.IsAssignableFrom(type)))
Expand Down
6 changes: 6 additions & 0 deletions src/xunit.analyzers/Utility/SerializableTypeSymbols.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ public sealed class SerializableTypeSymbols
readonly Lazy<INamedTypeSymbol?> dateOnly;
readonly Lazy<INamedTypeSymbol?> dateTimeOffset;
readonly Lazy<INamedTypeSymbol?> guid;
readonly Lazy<INamedTypeSymbol?> iFormattable;
readonly Lazy<INamedTypeSymbol?> index;
readonly Lazy<INamedTypeSymbol?> iParsableOfT;
readonly Lazy<INamedTypeSymbol?> iXunitSerializable;
readonly Lazy<INamedTypeSymbol?> range;
readonly Lazy<INamedTypeSymbol?> theoryDataBaseType;
Expand Down Expand Up @@ -40,7 +42,9 @@ public sealed class SerializableTypeSymbols
dateOnly = new(() => TypeSymbolFactory.DateOnly(compilation));
dateTimeOffset = new(() => TypeSymbolFactory.DateTimeOffset(compilation));
guid = new(() => TypeSymbolFactory.Guid(compilation));
iFormattable = new(() => TypeSymbolFactory.IFormattable(compilation));
index = new(() => TypeSymbolFactory.Index(compilation));
iParsableOfT = new(() => TypeSymbolFactory.IParsableOfT(compilation));
iXunitSerializable = new(() => xunitContext.Common.IXunitSerializableType);
range = new(() => TypeSymbolFactory.Range(compilation));
// For v2 and early versions of v3, the base type is "TheoryData" (non-generic). For later versions
Expand Down Expand Up @@ -83,7 +87,9 @@ public sealed class SerializableTypeSymbols
public INamedTypeSymbol? DateOnly => dateOnly.Value;
public INamedTypeSymbol? DateTimeOffset => dateTimeOffset.Value;
public INamedTypeSymbol? Guid => guid.Value;
public INamedTypeSymbol? IFormattable => iFormattable.Value;
public INamedTypeSymbol? Index => index.Value;
public INamedTypeSymbol? IParsableOfT => iParsableOfT.Value;
public INamedTypeSymbol? IXunitSerializable => iXunitSerializable.Value;
public INamedTypeSymbol MemberDataAttribute { get; }
public INamedTypeSymbol? Range => range.Value;
Expand Down
6 changes: 6 additions & 0 deletions src/xunit.analyzers/Utility/TypeSymbolFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ public static INamedTypeSymbol IEnumerableOfT(Compilation compilation) =>
return IEnumerableOfT(compilation).Construct(iTuple);
}

public static INamedTypeSymbol? IFormattable(Compilation compilation) =>
Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("System.IFormattable");

public static INamedTypeSymbol? IMessageSink_V2(Compilation compilation) =>
Guard.ArgumentNotNull(compilation).GetTypeByMetadataName(Constants.Types.Xunit.IMessageSink_V2);

Expand All @@ -184,6 +187,9 @@ public static INamedTypeSymbol IEnumerableOfT(Compilation compilation) =>
public static INamedTypeSymbol? IParameterInfo_V2(Compilation compilation) =>
Guard.ArgumentNotNull(compilation).GetTypeByMetadataName(Constants.Types.Xunit.IParameterInfo_V2);

public static INamedTypeSymbol? IParsableOfT(Compilation compilation) =>
Guard.ArgumentNotNull(compilation).GetTypeByMetadataName("System.IParsable`1");

public static INamedTypeSymbol IReadOnlyCollectionOfT(Compilation compilation) =>
Guard.ArgumentNotNull(compilation).GetSpecialType(SpecialType.System_Collections_Generic_IReadOnlyCollection_T);

Expand Down

0 comments on commit c485083

Please sign in to comment.