Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
0388e48
Consider all public types in a library as instantiated
vitek-karas Aug 31, 2021
39ffb9f
Change the solution to rely on the optimization setting only
vitek-karas Aug 31, 2021
e5b3788
wip
jtschuster Mar 25, 2022
61115f6
Add implicit interface implementation case
jtschuster Mar 28, 2022
c4ebd45
Edit tests to remove issue-specific code and names
jtschuster Mar 28, 2022
d7cc881
Mark members of CollectedType as kept
jtschuster Mar 28, 2022
3a8b76d
Add private interface test and simplify external interface example
jtschuster Mar 31, 2022
1c04ad2
Replace early exit for non-interfaces
jtschuster Apr 1, 2022
be37029
Merge branch 'main' into FixLibraryInterface
jtschuster Apr 1, 2022
7ad5afc
License headers and use IsMethodNeededByTypeDueToPreservedScope inste…
jtschuster Apr 1, 2022
07143dd
Add check for static methods before skipping virtual marking
jtschuster Apr 5, 2022
a7c7e13
Add more clarifying comments, move static method check, rename method
jtschuster Apr 6, 2022
64692a0
Renames and comment cleanup + tests
vitek-karas Apr 7, 2022
4ea2991
More tests for interface behavior
vitek-karas Apr 8, 2022
578c686
wip
jtschuster Apr 11, 2022
798e87f
Remove static interface methods that are only accessed by implemetations
jtschuster Apr 11, 2022
3fe5a52
remove commented out lines
jtschuster Apr 12, 2022
9908282
Lint and add deleted line
jtschuster Apr 12, 2022
3b03b77
Another test using interface through reflection
jtschuster Apr 12, 2022
4ae7888
Merge branch 'main' of https://github.com/dotnet/linker into trimStat…
jtschuster Apr 12, 2022
7df4117
Remove comments
jtschuster Apr 12, 2022
0b51131
Only remove override annotation if the static interface method is rem…
jtschuster Apr 13, 2022
e61826c
Add KeptOverride attribute and test support
jtschuster Apr 14, 2022
31a2447
Add Attribute files
jtschuster Apr 14, 2022
6e1aead
formatting
jtschuster Apr 14, 2022
52c4bb4
Move override removal to sweep step and add doc defining behavior
jtschuster Apr 18, 2022
854d08b
formatting
jtschuster Apr 19, 2022
14f71b9
Rework docs, rename test methods and types
jtschuster Apr 19, 2022
8edb5ac
Resolve overrides before sweeping
jtschuster Apr 19, 2022
0c79f06
formatting...
jtschuster Apr 19, 2022
ca0e253
Clarify wording in docs, move VerifyOverrides, rename vars
jtschuster Apr 20, 2022
c32a329
Add note about MethodImpl/Override marking
jtschuster Apr 20, 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
34 changes: 34 additions & 0 deletions src/linker/Linker.Steps/MarkStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,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 All @@ -582,6 +586,9 @@ void ProcessMarkedTypesWithInterfaces ()
var typesWithInterfaces = _typesWithInterfaces.ToArray ();

foreach ((var type, var scope) in typesWithInterfaces) {
// Regardless of the optimizations or whether or not the type is instantiated, we should
// remove unneeded override annotations for static interface method implementations
RemoveStaticInterfaceOverrideAnnotations (type);
// Exception, types that have not been flagged as instantiated yet. These types may not need their interfaces even if the
// interface type is marked
// UnusedInterfaces optimization is turned off mark all interface implementations
Expand Down Expand Up @@ -2265,6 +2272,26 @@ void MarkInterfaceImplementations (TypeDefinition type)
}
}

/// <summary>
/// Removes the 'override' annotation for implementations of static interface methods if the interface method is not marked.
/// </summary>
void RemoveStaticInterfaceOverrideAnnotations (TypeDefinition type)
Copy link
Member

Choose a reason for hiding this comment

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

Is there a way to write a test for this?
I don't know if the test infra actually verifies the "overrides" (maybe it should at least as an opt-in)

Copy link
Member Author

Choose a reason for hiding this comment

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

It doesn't look like there's anything in the test infra yet, but I'll look into it more and add a KeptOverride/NoOverride attribute to check for that

Copy link
Member Author

Choose a reason for hiding this comment

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

I've added a couple attribute to check for the presence/removal of the override annotations. They only perform a check if there is a KeptOverride or RemovedOverride, so it doesn't affect any other tests.

{
foreach (var method in type.Methods) {
if (!Annotations.IsMarked (method))
continue;
// Modify overrides in place
for (int i = 0; i < method.Overrides.Count;) {
if (Context.Resolve (method.Overrides[i].DeclaringType)?.IsInterface == true
&& Context.Resolve (method.Overrides[i])?.IsStatic == true
&& !Annotations.IsMarked (method.Overrides[i]))
method.Overrides.RemoveAt (i);
else
i++;
}
}
}

void MarkGenericParameterProvider (IGenericParameterProvider provider)
{
if (!provider.HasGenericParameters)
Expand Down Expand Up @@ -3049,8 +3076,15 @@ protected virtual void ProcessMethod (MethodDefinition method, in DependencyInfo
}
}

// Mark overrides except for static interface methods
Copy link
Member

Choose a reason for hiding this comment

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

Don't we want to mark the MethodImpl for these calls if and only if there's a constrained call to the target method?

Copy link
Member Author

@jtschuster jtschuster Apr 18, 2022

Choose a reason for hiding this comment

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

Yes, but here we are marking the methods themselves, not the MethodImpl. The static abstract interface methods themselves are marked elsewhere when the linker finds constrained calls to them. At the moment there's still no real "marking" of the MethodImpl, it just checks if the overridden method is marked, and if it's not, it removes the MethodImpl / Override.

We could separate marking the Overrides list from marking the Overridden methods themselves, but I feel like that would lead to more (issues where the Override / MethodImpl is incorrectly marked and the override method is correctly removed) than (issues where the Override / MethodImpl is correctly marked and the override method is incorrectly removed).

@sbomer do you have any thoughts on whether to mark overrides separately?

Copy link
Member

Choose a reason for hiding this comment

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

I actually like that we don't mark overrides/methodimpl explicitly:

  • With your change in the sweep step - there's no way to make the IL inconsistent - it will fix itself and linker just needs to decide if it keeps a certain method or on.
  • Linker in general doesn't "mutate" things, it only "deletes" them. So it should not be linker's decision if two methods should have the methodimpl between them or not - that decision should come from the input. If linker decides to keep both methods - the methodimpl between should be there if and only if it was there on the input.

Copy link
Member

Choose a reason for hiding this comment

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

Agreed, I like handling this in SweepStep and I like @vitek-karas's reasoning here.

if (method.HasOverrides) {
foreach (MethodReference ov in method.Overrides) {
// Don't mark overrides for static interface methods.
// Since they can only be called on a concrete type and not the interface, these methods can safely be removed in some cases
Copy link
Member

Choose a reason for hiding this comment

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

As discussed offline I wonder if this can be done for all interface methods (static and non-static) - but that's a separate discussion.
I think the comment here is a bit misleading, maybe a better way to describe this would be something like:

// Method implementing a static interface method will have an override to it.
// 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) {
continue;
}
MarkMethod (ov, new DependencyInfo (DependencyKind.MethodImplOverride, method));
MarkExplicitInterfaceImplementation (method, ov);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ public Task InstanceMethodSubstitutions ()
return RunTest (allowMissingWarnings: true);
}

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

[Fact]
public Task MethodWithParametersSubstitutions ()
{
Expand All @@ -55,6 +61,12 @@ public Task ReplacedReturns ()
return RunTest (allowMissingWarnings: true);
}

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

[Fact]
public Task SimpleConditionalProperty ()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;

namespace Mono.Linker.Tests.Cases.Expectations.Assertions
{
[AttributeUsage (AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
/// <Summary>
/// Used to ensure that a method should keep an 'override' annotation for a method in the supplied base type
/// Fails in tests if the method doesn't have the override method in the original or linked assembly
Copy link
Member

Choose a reason for hiding this comment

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

Please add a comment that this behaves differently from the other Kept attributes - almost all the other ones test both sides, that is if the attribute is not there it means the thing should NOT be in the output. For example if a certain type doesn't have a Kept attribute on it, we expected the type to not survive linking.
We do have some other attributes which are like this, but I think it's a good idea to at least document it - and maybe mention the other attribute RemoveOverrideAttribute as its counterpart.

/// </Summary>
public class KeptOverrideAttribute : KeptAttribute
{
public Type TypeWithOverriddenMethodDeclaration;

public KeptOverrideAttribute (Type typeWithOverriddenMethod)
{
if (typeWithOverriddenMethod == null)
throw new ArgumentNullException (nameof (typeWithOverriddenMethod));
TypeWithOverriddenMethodDeclaration = typeWithOverriddenMethod;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;

namespace Mono.Linker.Tests.Cases.Expectations.Assertions
{
/// <Summary>
/// Used to ensure that a method should remove an 'override' annotation for a method in the supplied base type.
/// Fails in tests if the method has the override method in the linked assembly,
/// or if the override is not found in the original assembly
/// </Summary>
[AttributeUsage (AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
public class RemovedOverrideAttribute : BaseInAssemblyAttribute
{
public Type TypeWithOverriddenMethodDeclaration;
public RemovedOverrideAttribute (Type typeWithOverriddenMethod)
{
if (typeWithOverriddenMethod == null)
throw new ArgumentException ("Value cannot be null or empty.", nameof (typeWithOverriddenMethod));
TypeWithOverriddenMethodDeclaration = typeWithOverriddenMethod;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,63 @@ public static void Main ()
t = typeof (UninstantiatedPublicClassWithImplicitlyImplementedInterface);
t = typeof (UninstantiatedPublicClassWithPrivateInterface);

UninstantiatedPublicClassWithInterface.InternalStaticInterfaceMethodUsed ();
InstantiatedClassWithInterfaces.InternalStaticInterfaceMethodUsed ();

UninstantiatedPublicClassWithInterface.InternalStaticInterfaceMethodUsedThroughImplementation ();
Copy link
Member

Choose a reason for hiding this comment

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

What's the significance of Internal here? It's repeated a lot throughout the code. I can't think of a reason why that would make a difference.

InstantiatedClassWithInterfaces.InternalStaticInterfaceMethodUsedThroughImplementation ();
GenericMethodThatCallsInternalStaticInterfaceMethod
<ImplementsInternalStaticInterfaceWithInterfaceDefinitionUsedThroughGeneric> ();
typeof (InterfaceVariants).GetMethod ("GenericMethodThatCallsInternalStaticInterfaceMethod")
.MakeGenericMethod (typeof (ImplementsInternalStaticInterfaceWithInterfaceDefinitionNotUsedThroughGeneric))
.Invoke (null, null);
// Use all public interfaces - they're marked as public only to denote them as "used"
typeof (IPublicInterface).RequiresPublicMethods ();
typeof (IPublicStaticInterface).RequiresPublicMethods ();

var a = new InstantiatedClassWithInterfaces ();
}

[Kept]
internal static void GenericMethodThatCallsInternalStaticInterfaceMethod<T> () where T : IInternalStaticInterfaceWithInterfaceDefinitionUsed
Copy link
Member

Choose a reason for hiding this comment

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

Personally, I think these names are getting long enough that they're not very readable, but I'll accede 😊

{
T.InternalStaticInterfaceUsedThroughInterface ();
}

[Kept]
[KeptInterface (typeof (IInternalStaticInterfaceWithInterfaceDefinitionUsed))]
internal class ImplementsInternalStaticInterfaceWithInterfaceDefinitionUsedThroughGeneric : IInternalStaticInterfaceWithInterfaceDefinitionUsed
{
[Kept]
[KeptOverride (typeof (IInternalStaticInterfaceWithInterfaceDefinitionUsed))]
public static void InternalStaticInterfaceUsedThroughInterface ()
{
}
public static void UnusedMethod () { }
}

[Kept]
[KeptInterface (typeof (IInternalStaticInterfaceWithInterfaceDefinitionUsed))]
internal class ImplementsInternalStaticInterfaceWithInterfaceDefinitionNotUsedThroughGeneric : IInternalStaticInterfaceWithInterfaceDefinitionUsed
Copy link
Member

Choose a reason for hiding this comment

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

I can't quite tell if this is meant to mean that the type is never used through a generic substitution. If so, that would mean that the InternalStaticInterfaceUsedThroughInterface method below has its override kept regardless.

I don't necessarily think this is the wrong behavior, but it's good to note. We might want to file a bug about a potential improvement in the future, if that's something we're interested in.

{
[Kept]
[KeptOverride (typeof (IInternalStaticInterfaceWithInterfaceDefinitionUsed))]
public static void InternalStaticInterfaceUsedThroughInterface ()
{
}

public static void UnusedMethod () { }
}

[Kept]
[KeptInterface (typeof (IEnumerator))]
[KeptInterface (typeof (IPublicInterface))]
[KeptInterface (typeof (IPublicStaticInterface))]
[KeptInterface (typeof (IInternalStaticInterfaceWithUsedMethod))] // https://github.com/dotnet/linker/issues/2733
[KeptInterface (typeof (ICopyLibraryInterface))]
[KeptInterface (typeof (ICopyLibraryStaticInterface))]
public class UninstantiatedPublicClassWithInterface :
IPublicInterface,
IPublicStaticInterface,
IInternalInterface,
IInternalStaticInterface,
IInternalStaticInterfaceWithUsedMethod,
IInternalStaticInterfaceWithUsedMethodImplementation,
IEnumerator,
ICopyLibraryInterface,
ICopyLibraryStaticInterface
Expand Down Expand Up @@ -70,7 +104,8 @@ public static void InternalStaticInterfaceMethod () { }
static void IInternalStaticInterface.ExplicitImplementationInternalStaticInterfaceMethod () { }

[Kept]
public static void InternalStaticInterfaceMethodUsed () { }
[RemovedOverride (typeof (IInternalStaticInterfaceWithUsedMethodImplementation))]
public static void InternalStaticInterfaceMethodUsedThroughImplementation () { }

[Kept]
[ExpectBodyModified]
Expand Down Expand Up @@ -122,15 +157,14 @@ public string ToString (string format, IFormatProvider formatProvider)
[KeptInterface (typeof (IEnumerator))]
[KeptInterface (typeof (IPublicInterface))]
[KeptInterface (typeof (IPublicStaticInterface))]
[KeptInterface (typeof (IInternalStaticInterfaceWithUsedMethod))] // https://github.com/dotnet/linker/issues/2733
[KeptInterface (typeof (ICopyLibraryInterface))]
[KeptInterface (typeof (ICopyLibraryStaticInterface))]
public class InstantiatedClassWithInterfaces :
IPublicInterface,
IPublicStaticInterface,
IInternalInterface,
IInternalStaticInterface,
IInternalStaticInterfaceWithUsedMethod,
IInternalStaticInterfaceWithUsedMethodImplementation,
IEnumerator,
ICopyLibraryInterface,
ICopyLibraryStaticInterface
Expand Down Expand Up @@ -159,7 +193,8 @@ public static void InternalStaticInterfaceMethod () { }
static void IInternalStaticInterface.ExplicitImplementationInternalStaticInterfaceMethod () { }

[Kept]
public static void InternalStaticInterfaceMethodUsed () { }
[RemovedOverride (typeof (IInternalStaticInterfaceWithUsedMethodImplementation))]
public static void InternalStaticInterfaceMethodUsedThroughImplementation () { }

[Kept]
bool IEnumerator.MoveNext () { throw new PlatformNotSupportedException (); }
Expand Down Expand Up @@ -225,13 +260,20 @@ internal interface IInternalStaticInterface
static abstract void ExplicitImplementationInternalStaticInterfaceMethod ();
}

// The interface methods themselves are not used, but the implentation of these methods is
// https://github.com/dotnet/linker/issues/2733
// The interface methods themselves are not used, but the implementation of these methods is
internal interface IInternalStaticInterfaceWithUsedMethodImplementation
{
static abstract void InternalStaticInterfaceMethodUsedThroughImplementation ();
}

// The interface methods themselves are used through the interface
[Kept]
internal interface IInternalStaticInterfaceWithUsedMethod
internal interface IInternalStaticInterfaceWithInterfaceDefinitionUsed
{
[Kept] // https://github.com/dotnet/linker/issues/2733
static abstract void InternalStaticInterfaceMethodUsed ();
[Kept]
static abstract void InternalStaticInterfaceUsedThroughInterface ();

static abstract void UnusedMethod ();
}

private interface IPrivateInterface
Expand Down
3 changes: 2 additions & 1 deletion test/Mono.Linker.Tests.Cases/Libraries/RootLibrary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ public void InternalInterfaceMethod () { }
void IInternalInterface.ExplicitImplementationInternalInterfaceMethod () { }

[Kept]
[RemovedOverride (typeof (IInternalStaticInterface))]
public static void InternalStaticInterfaceMethod () { }

static void IInternalStaticInterface.ExplicitImplementationInternalStaticInterfaceMethod () { }
Expand Down Expand Up @@ -300,6 +301,7 @@ public void InternalInterfaceMethod () { }
void IInternalInterface.ExplicitImplementationInternalInterfaceMethod () { }

[Kept]
[RemovedOverride (typeof (IInternalStaticInterface))]
public static void InternalStaticInterfaceMethod () { }

static void IInternalStaticInterface.ExplicitImplementationInternalStaticInterfaceMethod () { }
Expand Down Expand Up @@ -366,7 +368,6 @@ internal interface IInternalInterface
[Kept]
internal interface IInternalStaticInterface
{
[Kept] // https://github.com/dotnet/linker/issues/2733
static abstract void InternalStaticInterfaceMethod ();

static abstract void ExplicitImplementationInternalStaticInterfaceMethod ();
Expand Down
33 changes: 32 additions & 1 deletion test/Mono.Linker.Tests/TestCasesRunner/AssemblyChecker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,9 @@ protected virtual void VerifyTypeDefinitionKept (TypeDefinition original, TypeDe
if (verifiedEventMethods.Contains (m.FullName))
continue;
var msign = m.GetSignature ();
VerifyMethod (m, linked?.Methods.FirstOrDefault (l => msign == l.GetSignature ()));
var linkedMethod = linked?.Methods.FirstOrDefault (l => msign == l.GetSignature ());
VerifyMethod (m, linkedMethod);
VerifyOverrides (m, linkedMethod);
linkedMembers.Remove (m.FullName);
}
}
Expand Down Expand Up @@ -212,6 +214,35 @@ void VerifyInterfaces (TypeDefinition src, TypeDefinition linked)
}
}

void VerifyOverrides (MethodDefinition original, MethodDefinition linked)
{
if (linked is null)
return;
var expectedBaseTypesOverridden = new HashSet<string> (original.CustomAttributes
.Where (ca => ca.AttributeType.Name == nameof (KeptOverrideAttribute))
.Select (ca => (ca.ConstructorArguments[0].Value as TypeDefinition).FullName));
var originalBaseTypesOverridden = new HashSet<string> (original.Overrides.Select (ov => ov.DeclaringType.FullName));
var linkedBaseTypesOverridden = new HashSet<string> (linked.Overrides.Select (ov => ov.DeclaringType.FullName));
foreach (var expectedBaseType in expectedBaseTypesOverridden) {
Assert.IsTrue (originalBaseTypesOverridden.Contains (expectedBaseType),
$"Method {linked.FullName} was expected to keep override {expectedBaseType}::{linked.Name}, " +
"but it wasn't in the unlinked assembly");
Assert.IsTrue (linkedBaseTypesOverridden.Contains (expectedBaseType),
$"Method {linked.FullName} was expected to override {expectedBaseType}::{linked.Name}");
}

var expectedBaseTypesNotOverridden = new HashSet<string> (original.CustomAttributes
.Where (ca => ca.AttributeType.Name == nameof (RemovedOverrideAttribute))
.Select (ca => (ca.ConstructorArguments[0].Value as TypeDefinition).FullName));
foreach (var expectedRemovedBaseType in expectedBaseTypesNotOverridden) {
Assert.IsTrue (originalBaseTypesOverridden.Contains (expectedRemovedBaseType),
$"Method {linked.FullName} was expected to remove override {expectedRemovedBaseType}::{linked.Name}, " +
$"but it wasn't in the unlinked assembly");
Assert.IsFalse (linkedBaseTypesOverridden.Contains (expectedRemovedBaseType),
$"Method {linked.FullName} was expected to not override {expectedRemovedBaseType}::{linked.Name}");
}
}

static string FormatBaseOrInterfaceAttributeValue (CustomAttribute attr)
{
if (attr.ConstructorArguments.Count == 1)
Expand Down