diff --git a/src/ErrorProne.NET.CoreAnalyzers.Tests/CoreAnalyzers/DefaultToStringConversionAnalyzerTests.cs b/src/ErrorProne.NET.CoreAnalyzers.Tests/CoreAnalyzers/DefaultToStringConversionAnalyzerTests.cs index 91c53c8..4fe9e20 100644 --- a/src/ErrorProne.NET.CoreAnalyzers.Tests/CoreAnalyzers/DefaultToStringConversionAnalyzerTests.cs +++ b/src/ErrorProne.NET.CoreAnalyzers.Tests/CoreAnalyzers/DefaultToStringConversionAnalyzerTests.cs @@ -268,6 +268,40 @@ class Test { void T(Task t) => Foo(t); void Foo(object o) {} } +"; + await Verify.VerifyAsync(test); + } + + [Test] + public async Task NoWarn_When_Type_Has_Implicit_Conversion_To_String() + { + // Issue #317: a type that defines 'public static implicit operator string' + // opts into its own string representation, so EPC20 should not fire. + var test = @" +public class AmazonFile +{ + public static implicit operator string(AmazonFile f) => ""x""; +} +class Test { + string Concat(AmazonFile f) => ""x"" + f; + string Interp(AmazonFile f) => $""{f}""; + string Format(AmazonFile f) => string.Format(""{0}"", f); +} +"; + await Verify.VerifyAsync(test); + } + + [Test] + public async Task Warn_When_Implicit_Conversion_Returns_Something_Other_Than_String() + { + var test = @" +public class AmazonFile +{ + public static implicit operator int(AmazonFile f) => 0; +} +class Test { + string Concat(AmazonFile f) => ""x"" + [|f|]; +} "; await Verify.VerifyAsync(test); } diff --git a/src/ErrorProne.NET.CoreAnalyzers/CoreAnalyzers/DefaultToStringImplementationUsageAnalyzer.cs b/src/ErrorProne.NET.CoreAnalyzers/CoreAnalyzers/DefaultToStringImplementationUsageAnalyzer.cs index 72e3ddf..92a6c23 100644 --- a/src/ErrorProne.NET.CoreAnalyzers/CoreAnalyzers/DefaultToStringImplementationUsageAnalyzer.cs +++ b/src/ErrorProne.NET.CoreAnalyzers/CoreAnalyzers/DefaultToStringImplementationUsageAnalyzer.cs @@ -50,6 +50,13 @@ protected override bool TryCreateDiagnostic(Compilation compilation, ITypeSymbol return false; } + if (HasImplicitConversionToString(type)) + { + // The type opts into a custom string representation via 'public static implicit operator string', + // so a default ToString impl is not what will actually be used. See issue #317. + return false; + } + if (NoToStringOverride(type, out var typeWithNoToString)) { diagnostic = Diagnostic.Create(Rule, location, typeWithNoToString); @@ -58,6 +65,20 @@ protected override bool TryCreateDiagnostic(Compilation compilation, ITypeSymbol return diagnostic != null; } + private static bool HasImplicitConversionToString(ITypeSymbol type) + { + foreach (var member in type.GetMembers(WellKnownMemberNames.ImplicitConversionName)) + { + if (member is IMethodSymbol { MethodKind: MethodKind.Conversion } method && + method.ReturnType.SpecialType == SpecialType.System_String) + { + return true; + } + } + + return false; + } + private static bool NoToStringOverride(ITypeSymbol type, [NotNullWhen(true)]out ITypeSymbol? typeWithNoToString) { typeWithNoToString = null;