Fix #3180: Spell out type arguments of a null-conditional extension call

When the extension method call cannot be written without type arguments
(another extension method with the same name and a different arity is
found first), the call falls back to the static form. For 'a?.M()' that
fallback produced 'Enumerable.Count(s?)', which is not valid C#. Writing
the type arguments excludes the extension methods of a different arity,
so for a null-conditional target the type arguments are tried before
giving up.
This commit is contained in:
dualfroz committed 2026-10-03 11:46:13 +02:00
1 parent a6526a1dab
commit 8c8b8caad8
3 files changed
+52 -5

No files matched your search

@@ -932,6 +932,12 @@ namespace ICSharpCode.Decompiler.Tests
await RunForLibrary(cscOptions: cscOptions); await RunForLibrary(cscOptions: cscOptions);
} }
[Test]
public async Task Issue3180([ValueSource(nameof(roslynOnlyOptions))] CompilerOptions cscOptions)
{
await RunForLibrary(cscOptions: cscOptions);
}
[Test] [Test]
public async Task AssemblyCustomAttributes([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions) public async Task AssemblyCustomAttributes([ValueSource(nameof(defaultOptions))] CompilerOptions cscOptions)
{ {
@@ -0,0 +1,31 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty.Issue3180
{
internal static class Extensions
{
public static int Count(this IEnumerable source)
{
return 5;
}
}
internal class NullConditional
{
public static int StaticCall(IEnumerable<string> s)
{
return Enumerable.Count(s);
}
public static int CountOrZero(IEnumerable<string> s)
{
#if EXPECTED_OUTPUT
return s?.Count<string>() ?? 0;
#else
return (s != null) ? Enumerable.Count(s) : 0;
#endif
}
}
}
+15 -5
View File
@@ -1318,8 +1318,8 @@ namespace ICSharpCode.Decompiler.CSharp
return false; return false;
var firstArgument = argumentList.Arguments[0]; var firstArgument = argumentList.Arguments[0];
bool writeTypeArguments = (transform & ReferenceTransformation.RequireTypeArguments) != 0 bool canWriteTypeArguments = !settings.AnonymousTypes || !method.TypeArguments.Any(a => a.ContainsAnonymousType());
&& (!settings.AnonymousTypes || !method.TypeArguments.Any(a => a.ContainsAnonymousType())); bool writeTypeArguments = (transform & ReferenceTransformation.RequireTypeArguments) != 0 && canWriteTypeArguments;
IType[] typeArguments = writeTypeArguments ? method.TypeArguments.ToArray() : Empty<IType>.Array; IType[] typeArguments = writeTypeArguments ? method.TypeArguments.ToArray() : Empty<IType>.Array;
var directionExpression = firstArgument.Expression as DirectionExpression; var directionExpression = firstArgument.Expression as DirectionExpression;
@@ -1342,10 +1342,20 @@ namespace ICSharpCode.Decompiler.CSharp
string[]? remainingNames = argumentNames?.Take(actualArgumentCount).Skip(1).ToArray(); string[]? remainingNames = argumentNames?.Take(actualArgumentCount).Skip(1).ToArray();
if (remainingNames != null && remainingNames.All(name => name == null)) if (remainingNames != null && remainingNames.All(name => name == null))
remainingNames = null; remainingNames = null;
if (!resolver.CanTransformToExtensionMethodCall(method, typeArguments, target, var remainingArguments = argumentList.GetArgumentResolveResults(skipCount: 1).ToArray();
argumentList.GetArgumentResolveResults(skipCount: 1).ToArray(), remainingNames)) if (!resolver.CanTransformToExtensionMethodCall(method, typeArguments, target, remainingArguments, remainingNames))
{ {
return false; // 'a?.M()' has no static call form to fall back to. Spelled-out type arguments rule out
// extension methods of a different arity, which NullPropagationTransform relies on.
if (writeTypeArguments || !canWriteTypeArguments || method.TypeArguments.Count == 0
|| !IsNullConditional(firstArgument.Expression))
{
return false;
}
writeTypeArguments = true;
typeArguments = method.TypeArguments.ToArray();
if (!resolver.CanTransformToExtensionMethodCall(method, typeArguments, target, remainingArguments, remainingNames))
return false;
} }
Expression targetExpression; Expression targetExpression;