Skip to content

Commit 7cd8459

Browse files
authored
ILLink: Sweep .override for interface method if the .interfaceImpl is removed (#102857)
When static interface methods are kept but an implementation of the method is only accessed via a direct call on the implementing type, the implementation method and the .override pointing to the interface method are both kept, but not the .interfaceImpl. This causes a TypeLoadException when the .override points to a method on an interface that the type doesn't implement. To fix this, we needed to update the condition for sweeping overrides to include the case where both the interface and the interface method are kept, but the .interfaceImpl is not kept. Tests are for static interface methods, but concrete and generic to test type resolution in SweepStep, which can be finicky. Instance methods shouldn't hit this issue since public interface methods don't have a .override and private methods can't be marked without instantiating the type or reflecting over the type, both of which mark the type's .interfaceImpls, and making the .override valid. For instance methods, we will always mark any methods referenced in a .override as well as their corresponding interface implementation, so we won't end up in a scenario where an instance method would have dangling references in a .override. I did fix up the interface implementation marking (MarkRuntimeInterfaceImplementation) to make sure it can find a recursive interface, and marks the .interfaceImpl with the exact same TypeReference rather than just the same TypeDefinition (for example, IGeneric<int> should be marked, not just the first IGeneric<> interfaceImpl). This was one of the examples where the trimmer would end up doing the right thing (all IGeneric<> implementations would end up marked anyway), but it would do it for a different reason than we expect.
1 parent c7ba33f commit 7cd8459

17 files changed

+1134
-192
lines changed

src/tools/illink/src/linker/Linker.Steps/MarkStep.cs

+28-35
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@
3535
using System.Diagnostics;
3636
using System.Diagnostics.CodeAnalysis;
3737
using System.Linq;
38-
using System.Reflection.Metadata.Ecma335;
3938
using System.Reflection.Runtime.TypeParsing;
40-
using System.Runtime.CompilerServices;
4139
using System.Text.RegularExpressions;
4240
using ILCompiler.DependencyAnalysisFramework;
4341
using ILLink.Shared;
@@ -718,33 +716,25 @@ void MarkMethodIfNeededByBaseMethod (MethodDefinition method, MessageOrigin orig
718716
/// or if any marked interface implementations on <paramref name="type"/> are interfaces that implement <paramref name="interfaceType"/> and that interface implementation is marked
719717
/// </summary>
720718
bool IsInterfaceImplementationMarkedRecursively (TypeDefinition type, TypeDefinition interfaceType)
719+
=> IsInterfaceImplementationMarkedRecursively (type, interfaceType, Context);
720+
721+
/// <summary>
722+
/// Returns true if <paramref name="type"/> implements <paramref name="interfaceType"/> and the interface implementation is marked,
723+
/// or if any marked interface implementations on <paramref name="type"/> are interfaces that implement <paramref name="interfaceType"/> and that interface implementation is marked
724+
/// </summary>
725+
internal static bool IsInterfaceImplementationMarkedRecursively (TypeDefinition type, TypeDefinition interfaceType, LinkContext context)
721726
{
722727
if (type.HasInterfaces) {
723728
foreach (var intf in type.Interfaces) {
724-
TypeDefinition? resolvedInterface = Context.Resolve (intf.InterfaceType);
729+
TypeDefinition? resolvedInterface = context.Resolve (intf.InterfaceType);
725730
if (resolvedInterface == null)
726731
continue;
727-
728-
if (Annotations.IsMarked (intf) && RequiresInterfaceRecursively (resolvedInterface, interfaceType))
729-
return true;
730-
}
731-
}
732-
733-
return false;
734-
}
735-
736-
bool RequiresInterfaceRecursively (TypeDefinition typeToExamine, TypeDefinition interfaceType)
737-
{
738-
if (typeToExamine == interfaceType)
739-
return true;
740-
741-
if (typeToExamine.HasInterfaces) {
742-
foreach (var iface in typeToExamine.Interfaces) {
743-
var resolved = Context.TryResolve (iface.InterfaceType);
744-
if (resolved == null)
732+
if (!context.Annotations.IsMarked (intf))
745733
continue;
746734

747-
if (RequiresInterfaceRecursively (resolved, interfaceType))
735+
if (resolvedInterface == interfaceType)
736+
return true;
737+
if (IsInterfaceImplementationMarkedRecursively (resolvedInterface, interfaceType, context))
748738
return true;
749739
}
750740
}
@@ -3197,8 +3187,12 @@ protected virtual void ProcessMethod (MethodDefinition method, in DependencyInfo
31973187
Context.Resolve (@base) is MethodDefinition baseDefinition
31983188
&& baseDefinition.DeclaringType.IsInterface && baseDefinition.IsStatic && method.IsStatic)
31993189
continue;
3190+
// Instance methods can have overrides on public implementation methods in IL, but C# will usually only have them for private explicit interface implementations.
3191+
// It is valid IL for a public method to override an interface method and only be called directly. In this case it would be safe to skip marking the .override method.
3192+
// However, in most cases, the C# compiler will only generate .override for instance methods when it's a private explicit interface implementations which can only be called through the interface.
3193+
// We can just take a short cut and mark all the overrides on instance methods. We shouldn't miss out on size savings for code generated by Roslyn.
32003194
MarkMethod (@base, new DependencyInfo (DependencyKind.MethodImplOverride, method), methodOrigin);
3201-
MarkExplicitInterfaceImplementation (method, @base);
3195+
MarkRuntimeInterfaceImplementation (method, @base);
32023196
}
32033197
}
32043198

@@ -3314,22 +3308,21 @@ protected virtual void MarkRequirementsForInstantiatedTypes (TypeDefinition type
33143308
DoAdditionalInstantiatedTypeProcessing (type);
33153309
}
33163310

3317-
void MarkExplicitInterfaceImplementation (MethodDefinition method, MethodReference ov)
3311+
void MarkRuntimeInterfaceImplementation (MethodDefinition method, MethodReference ov)
33183312
{
33193313
if (Context.Resolve (ov) is not MethodDefinition resolvedOverride)
33203314
return;
3315+
if (!resolvedOverride.DeclaringType.IsInterface)
3316+
return;
3317+
var interfaceToBeImplemented = ov.DeclaringType;
33213318

3322-
if (resolvedOverride.DeclaringType.IsInterface) {
3323-
foreach (var ifaceImpl in method.DeclaringType.Interfaces) {
3324-
var resolvedInterfaceType = Context.Resolve (ifaceImpl.InterfaceType);
3325-
if (resolvedInterfaceType == null) {
3326-
continue;
3327-
}
3328-
3329-
if (resolvedInterfaceType == resolvedOverride.DeclaringType) {
3330-
MarkInterfaceImplementation (ifaceImpl, new MessageOrigin (method.DeclaringType));
3331-
return;
3332-
}
3319+
var ifaces = Annotations.GetRecursiveInterfaces (method.DeclaringType);
3320+
if (ifaces is null)
3321+
return;
3322+
foreach (var iface in ifaces) {
3323+
if (TypeReferenceEqualityComparer.AreEqual (iface.InterfaceType, interfaceToBeImplemented, Context)) {
3324+
MarkInterfaceImplementationList (iface.ImplementationChain, new MessageOrigin (method.DeclaringType));
3325+
return;
33333326
}
33343327
}
33353328
}

src/tools/illink/src/linker/Linker.Steps/SweepStep.cs

+8-3
Original file line numberDiff line numberDiff line change
@@ -456,14 +456,19 @@ void SweepOverrides (MethodDefinition method)
456456
// This can happen for a couple of reasons, but it indicates the method isn't in the final assembly.
457457
// Resolve also may return a removed value if method.Overrides[i] is a MethodDefinition. In this case, Resolve short circuits and returns `this`.
458458
// OR
459-
// ov.DeclaringType is null
459+
// ov.DeclaringType is null
460460
// ov.DeclaringType may be null if Resolve short circuited and returned a removed method. In this case, we want to remove the override.
461461
// OR
462-
// ov is in a `link` scope and is unmarked
462+
// ov is in a `link` scope and is unmarked
463463
// ShouldRemove returns true if the method is unmarked, but we also We need to make sure the override is in a link scope.
464464
// Only things in a link scope are marked, so ShouldRemove is only valid for items in a `link` scope.
465+
// OR
466+
// ov is an interface method and the interface is not implemented by the type
465467
#pragma warning disable RS0030 // Cecil's Resolve is banned - it's necessary when the metadata graph isn't stable
466-
if (method.Overrides[i].Resolve () is not MethodDefinition ov || ov.DeclaringType is null || (IsLinkScope (ov.DeclaringType.Scope) && ShouldRemove (ov)))
468+
if (method.Overrides[i].Resolve () is not MethodDefinition ov
469+
|| ov.DeclaringType is null
470+
|| (IsLinkScope (ov.DeclaringType.Scope) && ShouldRemove (ov))
471+
|| (ov.DeclaringType.IsInterface && !MarkStep.IsInterfaceImplementationMarkedRecursively (method.DeclaringType, ov.DeclaringType, Context)))
467472
method.Overrides.RemoveAt (i);
468473
else
469474
i++;

src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/StaticInterfaceMethods.cs

+6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ public Task BaseProvidesInterfaceMethod ()
1616
return RunTest (allowMissingWarnings: false);
1717
}
1818

19+
[Fact]
20+
public Task RemovedInterfaceImplementationRemovedOverride()
21+
{
22+
return RunTest (allowMissingWarnings: false);
23+
}
24+
1925
[Fact]
2026
public Task StaticAbstractInterfaceMethods ()
2127
{

src/tools/illink/test/Mono.Linker.Tests.Cases.Expectations/Assertions/KeptOverrideAttribute.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ namespace Mono.Linker.Tests.Cases.Expectations.Assertions
1515
[AttributeUsage (AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
1616
public class KeptOverrideAttribute : KeptAttribute
1717
{
18-
public Type TypeWithOverriddenMethodDeclaration;
19-
2018
public KeptOverrideAttribute (Type typeWithOverriddenMethod)
2119
{
2220
ArgumentNullException.ThrowIfNull (typeWithOverriddenMethod);
23-
TypeWithOverriddenMethodDeclaration = typeWithOverriddenMethod;
21+
}
22+
public KeptOverrideAttribute (string typeWithOverriddenMethod)
23+
{
2424
}
2525
}
26-
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
6+
namespace Mono.Linker.Tests.Cases.Expectations.Assertions
7+
{
8+
[AttributeUsage (AttributeTargets.All, AllowMultiple = true)]
9+
public class KeptOverrideOnMethodInAssemblyAttribute : BaseInAssemblyAttribute
10+
{
11+
public KeptOverrideOnMethodInAssemblyAttribute (string assemblyName, string typeName, string methodName, string overriddenMethodName)
12+
{
13+
}
14+
}
15+
}

src/tools/illink/test/Mono.Linker.Tests.Cases.Expectations/Assertions/RemovedOverrideAttribute.cs

+5-3
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ namespace Mono.Linker.Tests.Cases.Expectations.Assertions
1414
[AttributeUsage (AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
1515
public class RemovedOverrideAttribute : BaseInAssemblyAttribute
1616
{
17-
public Type TypeWithOverriddenMethodDeclaration;
1817
public RemovedOverrideAttribute (Type typeWithOverriddenMethod)
1918
{
2019
if (typeWithOverriddenMethod == null)
2120
throw new ArgumentException ("Value cannot be null or empty.", nameof (typeWithOverriddenMethod));
22-
TypeWithOverriddenMethodDeclaration = typeWithOverriddenMethod;
21+
}
22+
23+
public RemovedOverrideAttribute (string nameOfOverriddenMethod)
24+
{
2325
}
2426
}
25-
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
6+
namespace Mono.Linker.Tests.Cases.Expectations.Assertions
7+
{
8+
[AttributeUsage (AttributeTargets.All, AllowMultiple = true, Inherited = false)]
9+
public class RemovedOverrideOnMethodInAssemblyAttribute : BaseInAssemblyAttribute
10+
{
11+
public RemovedOverrideOnMethodInAssemblyAttribute (string library, string typeName, string methodName, string overriddenMethodFullName)
12+
{
13+
}
14+
}
15+
}

0 commit comments

Comments
 (0)