Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f704291
Re-add static interface trimming with more testing
jtschuster May 9, 2022
68c530d
Add static interface tests for library mode
jtschuster May 12, 2022
97cd617
Add sanity check for overrides
jtschuster May 12, 2022
8190bf7
Add sanity check for overrides
jtschuster May 12, 2022
9a5fe0f
Merge branch 'main' into overrideSanityCheck
jtschuster May 12, 2022
3a560d4
Merge branch 'overrideSanityCheck' into staticInterfacesBack
jtschuster May 12, 2022
733bf90
Update comment on override removal in sweep step
jtschuster May 12, 2022
47104d9
Add attributes
jtschuster May 12, 2022
c2f4fd1
formatting
jtschuster May 12, 2022
3d577ce
formatting
jtschuster May 12, 2022
8ac16bb
Merge branch 'overrideSanityCheck' into staticInterfacesBack
jtschuster May 13, 2022
cccca4c
Modify override removal logic and update comment
jtschuster May 13, 2022
c380027
Mark override on explicit implementation
jtschuster May 13, 2022
4813047
Tweak override removal comment
jtschuster May 13, 2022
332e79f
wip
jtschuster May 13, 2022
9f64a32
Merge branch 'main' into staticInterfacesBack
jtschuster May 25, 2022
9d27c3b
Merge remote-tracking branch 'upstream/main' into staticInterfacesBack
jtschuster May 25, 2022
7f1064d
Choose correct merge conflict
jtschuster May 25, 2022
15db395
use FullName in comparison
jtschuster May 26, 2022
1751697
wip
jtschuster Jun 1, 2022
b10cd5f
Update docs/removal-behavior.md
jtschuster Jun 1, 2022
e3fbf12
Add explanation for workaround to remove interface implementation
jtschuster Jun 1, 2022
098cb24
Merge branch 'staticInterfacesBack' of https://github.com/jtschuster/…
jtschuster Jun 1, 2022
7467d82
wip
jtschuster Jun 2, 2022
4efa5f2
Mark static interface implmentations in ProcessOverride
jtschuster Jun 2, 2022
c3a26ca
Format and update comment
jtschuster Jun 2, 2022
62e3cc8
Use separate list for static interface methods
jtschuster Jun 3, 2022
4359792
Add clarifying comments
jtschuster Jun 3, 2022
060345e
Roll early exit case into function
jtschuster Jun 3, 2022
a050fdf
Move check back outside of interface override method
jtschuster Jun 7, 2022
95df541
formatting
jtschuster Jun 7, 2022
2b7a7cc
Use Override pair to store logic for IsStaticInterfaceMethodPair
jtschuster Jun 7, 2022
c00e1d1
Formatting
jtschuster Jun 8, 2022
358b010
Move interfaceImpl marking out of ProcessOverride
jtschuster Jun 13, 2022
5672f0f
Merge remote-tracking branch 'upstream/main' into staticInterfacesBack
jtschuster Jun 14, 2022
432375d
PR Feedback
jtschuster Jun 14, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/removal-behavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public class Program

### Method call on a constrained type parameter

On a call to a static abstract interface method that is accessed through a constrained type parameter, the interface method is rooted, as well as every implementation method on every type.
On a call to a static abstract interface method that is accessed through a constrained type parameter, the interface method is rooted, as well as every implementation method on every type that is rooted.

Example:

Expand Down
14 changes: 14 additions & 0 deletions src/linker/Linker.Steps/MarkStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,10 @@ void ProcessVirtualMethods ()
}
}

/// <summary>
/// Does extra handling of marked types that have interfaces when it's necessary to know what types are marked or instantiated.
/// e.g. Marks the "implements interface" annotations and removes override annotations for static interface methods.
/// </summary>
void ProcessMarkedTypesWithInterfaces ()
{
// We may mark an interface type later on. Which means we need to reprocess any time with one or more interface implementations that have not been marked
Expand Down Expand Up @@ -3017,8 +3021,18 @@ protected virtual void ProcessMethod (MethodDefinition method, in DependencyInfo
}
}

// Mark overridden methods except for static interface methods
if (method.HasOverrides) {
foreach (MethodReference ov in method.Overrides) {
// Method implementing a static interface method will have an override to it - note nonstatic methods usually don't unless they're explicit.
// Calling the implementation method directly has no impact on the interface, and as such it should not mark the interface or its method.
// Only if the interface method is referenced, then all the methods which implemented must be kept, but not the other way round.
if (Context.Resolve (ov)?.IsStatic == true
&& Context.Resolve (ov.DeclaringType)?.IsInterface == true
// Public methods can be called directly on the type. If it non-public it is an explicit implementation and if the method is marked we need to mark the override.
&& method.IsPublic) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What goes wrong if we don't mark the interface method for non-public explicit implementations here? I understand that the non-public implementation will only be reached through the interface method, but I would have roughly expected that logic elsewhere, in ProcessMarkedTypesWithInterfaces, would take care of that. This looks like it's doing the right thing but I would like to understand if it's necessary.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also prefer we remove the IsPublic check. What if it's internal and there InternalsVisibleTo applied - then it is effectively public. Visibility should have basically no effect here. The explicit implementation methods should be never accessed directly as C# doesn't let you do that - so the only way is via reflection, and in that case I think it's OK to overmark, definitely not something I would optimize for.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically speaking we should be able to avoid marking overrides for interface methods completely (not just for static interfaces) - but that's a bigger/unrelated change. (And it is unlikely to actually do much since non-static interfaces do not use overrides in C#, so the cases where this would happen are very limitted in real world).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I can tell, a method implementing an interface method must be public to implicitly implement the method or private for an explicit implementation, so I don't think we'd run into the internal with InternalsVisibleTo issue.

I think I could avoid of complication by looking from base -> override instead of override -> base like I do here. I didn't see the AnnotationStore.GetOverrides which does the inverse of Cecil's .Overrides. If I check if a method is a static interface method and kept, I can mark the methods in GetOverrides as well as the interface implementation.

I would have roughly expected that logic elsewhere, in ProcessMarkedTypesWithInterfaces
I agree, that would make sense to me, but at the moment that method returns early if the method isn't instantiated (or relevant to variant casting). If we move the logic for static interface methods, we'd have to iterate through each method to check if any override a static interface method before we can say we don't need an interface implementation. I think it could be worth it to keep the related logic in the same place though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I though that the reverse direction (from base to overrides) is already there in the ProcessMarkedTypesWithInterfaces (or similar). The code here is the opposite direction which is from implementation (override) to base.

Copy link
Member Author

@jtschuster jtschuster Jun 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think here we should keep marking interface implementations for explicitly implemented non-static interface methods. I moved the logic to mark static interface method overrides from base -> implementation to ProcessOverrides.

If we move marking the interface implementations for explicit interface method implementations to ProcessMArkedTypesWithInterfaces we would be duplicating work with ProcessMethod because we'd have to look at each method in the type to determine if we should keep the interface impl.

I think the way it is now makes the most sense to me, but I can rework if we think another way would be better.

continue;
}
MarkMethod (ov, new DependencyInfo (DependencyKind.MethodImplOverride, method), ScopeStack.CurrentScope.Origin);
MarkExplicitInterfaceImplementation (method, ov);
}
Expand Down
34 changes: 34 additions & 0 deletions src/linker/Linker.Steps/SweepStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,8 @@ protected virtual void SweepMethods (Collection<MethodDefinition> methods)

SweepCustomAttributes (method.MethodReturnType);

SweepOverrides (method);

if (!method.HasParameters)
continue;

Expand All @@ -467,6 +469,38 @@ protected virtual void SweepMethods (Collection<MethodDefinition> methods)
}
}

void SweepOverrides (MethodDefinition method)
{
for (int i = 0; i < method.Overrides.Count;) {
// We can't rely on the context resolution cache anymore, since it may remember methods which are already removed
// So call the direct Resolve here and avoid the cache.
// We want to remove a method from the list of Overrides if:
// Resolve() is null
// This can happen for a couple of reasons, but it indicates the method isn't in the final assembly.
// Resolve also may return a removed value if method.Overrides[i] is a MethodDefinition. In this case, Resolve short circuits and returns `this`.
// OR
// ov.DeclaringType is null
// ov.DeclaringType may be null if Resolve short circuited and returned a removed method. In this case, we want to remove the override.
// OR
// ov is in a `link` scope and is unmarked
// ShouldRemove returns true if the method is unmarked, but we also We need to make sure the override is in a link scope.
// Only things in a link scope are marked, so ShouldRemove is only valid for items in a `link` scope.
if (method.Overrides[i].Resolve () is not MethodDefinition ov || ov.DeclaringType is null || (IsLinkScope (ov.DeclaringType.Scope) && ShouldRemove (ov)))
method.Overrides.RemoveAt (i);
else
i++;
}
}

/// <summary>
/// Returns true if the assembly of the <paramref name="scope"></paramref> is set to link
/// </summary>
private bool IsLinkScope (IMetadataScope scope)
{
AssemblyDefinition? assembly = Context.Resolve (scope);
return assembly != null && Annotations.GetAction (assembly) == AssemblyAction.Link;
}

void SweepDebugInfo (Collection<MethodDefinition> methods)
{
List<ScopeDebugInformation>? sweptScopes = null;
Expand Down
3 changes: 3 additions & 0 deletions src/linker/Linker/Annotations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,9 @@ public bool IsPublic (IMetadataTokenProvider provider)
return public_api.Contains (provider);
}

/// <summary>
/// Returns an IEnumerable of the methods that override this method. Note this is different than <see cref="MethodDefinition.Overrides"/>, which returns the MethodImpl's
/// </summary>
public IEnumerable<OverrideInformation>? GetOverrides (MethodDefinition method)
{
return TypeMapInfo.GetOverrides (method);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using System.Threading.Tasks;
using Xunit;

namespace ILLink.RoslynAnalyzer.Tests.Inheritance.Interfaces
{
public sealed partial class StaticInterfaceMethodsTests : LinkerTestBase
{

protected override string TestSuiteName => "Inheritance.Interfaces.StaticInterfaceMethods";

[Fact]
public Task StaticAbstractInterfaceMethods ()
{
return RunTest (allowMissingWarnings: true);
}

[Fact]
public Task StaticAbstractInterfaceMethodsLibrary ()
{
return RunTest (allowMissingWarnings: true);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,41 +71,32 @@ internal class ImplementsUnusedStaticInterface
// The interface methods themselves are not used, but the implementation of these methods is
internal interface IStaticInterfaceMethodUnused
{
// Can be removed with Static Interface trimming optimization
[Kept]
static abstract void InterfaceUsedMethodNot ();
}

// Can be removed with Static Interface Trimming
[Kept]
internal interface IStaticInterfaceUnused
{
// Can be removed with Static Interface Trimming
[Kept]
static abstract void InterfaceAndMethodNoUsed ();
}

[Kept]
[KeptInterface (typeof (IStaticInterfaceUnused))]
[KeptInterface (typeof (IStaticInterfaceMethodUnused))]
internal class InterfaceMethodUsedThroughImplementation : IStaticInterfaceMethodUnused, IStaticInterfaceUnused
{
[Kept]
[RemovedOverride (typeof (IStaticInterfaceMethodUnused))]
public static void InterfaceUsedMethodNot () { }

[Kept]
[RemovedOverride (typeof (IStaticInterfaceUnused))]
public static void InterfaceAndMethodNoUsed () { }
}

[Kept]
[KeptInterface (typeof (IStaticInterfaceMethodUnused))]
[KeptInterface (typeof (IStaticInterfaceUnused))]
[KeptInterface (typeof (IStaticInterfaceMethodUnused))] // Bug
internal class InterfaceMethodUnused : IStaticInterfaceMethodUnused, IStaticInterfaceUnused
{
[Kept]
public static void InterfaceUsedMethodNot () { }

[Kept]
public static void InterfaceAndMethodNoUsed () { }
}

Expand Down
Loading