Browse Source

Report a call as unresolvable when nothing matched at all

Overload resolution reports no error for an empty candidate set - there is
no best candidate to attach one to - so the null result passed for success
and was dereferenced while checking the call target. Decompiling
FSharp.DataFrame from nuget.org crashes that way: F# compiles its comparison
members to instance methods carrying operator metadata names, and the
operator candidate search looks at the operand types rather than at the
receiver type the member belongs to.

The new fixture pins that such an assembly decompiles at all. It still
renders those instance methods as operators and drops the receiver at the
call sites, which is the misclassification behind the empty candidate set
and is handled separately; this is the guard that keeps an empty candidate
set from being read as a resolved call.

Assisted-by: Claude:claude-opus-5:Claude Code
pull/3995/head
Siegfried Pammer 1 month ago
parent
commit
a9c4f6c6bb
  1. 5
      ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
  2. 6
      ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs
  3. 24
      ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InstanceOperatorCall.cs
  4. 89
      ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InstanceOperatorCall.il
  5. 7
      ICSharpCode.Decompiler/CSharp/CallBuilder.cs

5
ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
@ -196,6 +196,9 @@ @@ -196,6 +196,9 @@
<None Include="TestCases\ILPretty\CS1xSwitch_Release.cs" />
<Compile Remove="TestCases\ILPretty\CallIndirect.cs" />
<None Include="TestCases\ILPretty\CallIndirect.cs" />
<Compile Remove="TestCases\ILPretty\InstanceOperatorCall.cs" />
<None Include="TestCases\ILPretty\InstanceOperatorCall.cs" />
<None Include="TestCases\ILPretty\InstanceOperatorCall.il" />
<Compile Remove="TestCases\ILPretty\EmptyBodies.cs" />
<None Include="TestCases\ILPretty\EmptyBodies.cs" />
<Compile Remove="TestCases\ILPretty\TruncatedAccessorBody.cs" />

6
ICSharpCode.Decompiler.Tests/ILPrettyTestRunner.cs

@ -333,6 +333,12 @@ namespace ICSharpCode.Decompiler.Tests @@ -333,6 +333,12 @@ namespace ICSharpCode.Decompiler.Tests
await Run();
}
[Test]
public async Task InstanceOperatorCall()
{
await Run();
}
[Test]
public async Task FSharpLoops_Debug()
{

24
ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InstanceOperatorCall.cs

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
public class CallSite
{
public static bool CompareVirtual<T>(Source<T> s, T a, T b)
{
return a < b;
}
public static bool CompareDirect(Source<int> s, int a, int b)
{
return a > b;
}
}
public class Source<T>
{
public virtual bool operator <(T a, T b)
{
return false;
}
public bool operator >(T a, T b)
{
return false;
}
}

89
ICSharpCode.Decompiler.Tests/TestCases/ILPretty/InstanceOperatorCall.il

@ -0,0 +1,89 @@ @@ -0,0 +1,89 @@
// Metadata version: v4.0.30319
.assembly extern System.Runtime
{
.publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A ) // .?_....:
.ver 11:0:0:0
}
.assembly InstanceOperatorCall
{
.ver 1:0:0:0
}
.module InstanceOperatorCall.dll
.imagebase 0x10000000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003 // WINDOWS_CUI
.corflags 0x00000001 // ILONLY
// F# emits comparison operators as instance members, so operator metadata names appear on
// methods C# would never bind as operators: the operands are the arguments, and the operator
// lives on the receiver type rather than on either operand type.
.class public auto ansi beforefieldinit Source`1<T>
extends [System.Runtime]System.Object
{
.method public hidebysig specialname newslot virtual
instance bool op_LessThan(!T a,
!T b) cil managed
{
.maxstack 8
IL_0000: ldc.i4.0
IL_0001: ret
}
.method public hidebysig specialname
instance bool op_GreaterThan(!T a,
!T b) cil managed
{
.maxstack 8
IL_0000: ldc.i4.0
IL_0001: ret
}
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [System.Runtime]System.Object::.ctor()
IL_0006: ret
}
}
.class public auto ansi beforefieldinit CallSite
extends [System.Runtime]System.Object
{
.method public hidebysig static bool CompareVirtual<T>(class Source`1<!!T> s,
!!T a,
!!T b) cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: ldarg.2
IL_0003: callvirt instance bool class Source`1<!!T>::op_LessThan(!0,
!0)
IL_0008: ret
}
.method public hidebysig static bool CompareDirect(class Source`1<int32> s,
int32 a,
int32 b) cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: ldarg.2
IL_0003: call instance bool class Source`1<int32>::op_GreaterThan(!0,
!0)
IL_0008: ret
}
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance void [System.Runtime]System.Object::.ctor()
IL_0006: ret
}
}

7
ICSharpCode.Decompiler/CSharp/CallBuilder.cs

@ -1643,6 +1643,13 @@ namespace ICSharpCode.Decompiler.CSharp @@ -1643,6 +1643,13 @@ namespace ICSharpCode.Decompiler.CSharp
if (or.IsAmbiguous)
return OverloadResolutionErrors.AmbiguousMatch;
foundMember = or.GetBestCandidateWithSubstitutedTypeArguments();
if (foundMember == null)
{
// Overload resolution reports no error for an empty candidate set - there is no
// best candidate to carry one - so a call that matched nothing has to be reported
// as unresolvable here.
return OverloadResolutionErrors.AmbiguousMatch;
}
if (!IsAppropriateCallTarget(expectedTargetDetails, method, foundMember))
return OverloadResolutionErrors.AmbiguousMatch;
var map = or.GetArgumentToParameterMap();

Loading…
Cancel
Save