Skip to content

Warn on called methods annotated with RequiresUnreferencedCode#1778

Merged
mateoatr merged 13 commits intodotnet:masterfrom
mateoatr:reqUnrefCode
Feb 1, 2021
Merged

Warn on called methods annotated with RequiresUnreferencedCode#1778
mateoatr merged 13 commits intodotnet:masterfrom
mateoatr:reqUnrefCode

Conversation

@mateoatr
Copy link
Contributor

See #1607. Most of the changes in this PR were already in #1639. We only run through ProcessRequiresUnreferencedCode whenever the annotated method was either called or found in a reflection scenario. A test case was added for making sure that methods marked with RequiresUnreferencedCodeAttribute in copied assemblies do not trigger any warnings (unless called or referenced through reflection).

}

[ExpectedWarning ("IL2026", "--MethodCalledThroughReflection--")]
static void TestRequiresThroughReflectionInMethodFromCopiedAssembly ()
Copy link
Member

Choose a reason for hiding this comment

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

Given the use case in DispatchProxy (that one is about DynamicallyAccessedMember, but it's similar) - can you please add another test case where we apply the All annotation to an interface and we validate that it warns if one of the methods on the interface is annotated with RequiresUnreferencedCode - maybe a more complex one if the method is on another interface, for example:

interface A { [RequiresUnreferencedCode] void DoA (); }
interface B : A { void DoB (); }

RequiresAll(typeof(B));

}
}

DependencyInfo GetDependencyInfo (IMemberDefinition member, DependencyInfo reason)
Copy link
Member

Choose a reason for hiding this comment

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

With the change to use an explicit parameter, this should not be needed for functionality. I don't mind making the change as it makes it easier to understand dependencies, but maybe we should make that in a separate PR.

foreach (InterfaceImplementation iface in type.Interfaces) {
MarkInterfaceImplementation (iface, type);
if (reason.Kind == DependencyKind.AccessedViaReflection)
MarkEntireTypeInternal (iface.InterfaceType.Resolve (), includeBaseTypes: true, reason, type, typesAlreadyVisited);
Copy link
Member

Choose a reason for hiding this comment

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

I think we need to call MarkInterfaceImplementation anyway. If the interface is a generic instantiation we would lose that information and for example would not correctly apply DynamicallyAccessedMembers.
I think we'll need to look into this a bit more - the same applies to base types, and potentially even methods. I filed https://github.com/mono/linker/issues/1787 to look into this some more.

Copy link
Member

Choose a reason for hiding this comment

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

Also - it's technically possible for Resole to return null, the should not crash on it. See handling of base types.

Comment on lines +2653 to +2654
if (reason.Kind == DependencyKind.AccessedViaReflection)
ProcessRequiresUnreferencedCode (method, new MessageOrigin (reason.Source as IMemberDefinition), reason.Kind);
Copy link
Member

Choose a reason for hiding this comment

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

This feels wrong - I understand it's because MarkEntireType doesn't call MarkMethod - maybe it should...
Can you also check if there are other places which side-step MarkMethod and end up here instead? Those could all be potential issues as well.

Annotations.Mark (method, GetDependencyInfo (method, reason));
Annotations.SetAction (method, MethodAction.ForceParse);
EnqueueMethod (method, GetDependencyInfo (method, reason));
MarkMethod (method, new DependencyInfo (reason.Kind == DependencyKind.AccessedViaReflection ? reason.Kind : DependencyKind.MemberOfType, type), new MessageOrigin (reason.Source as IMemberDefinition));
Copy link
Member

Choose a reason for hiding this comment

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

We should keep the SetAction - the difference in behavior will be pretty small, but still - no reason to NOT do that.

Copy link
Member

@vitek-karas vitek-karas left a comment

Choose a reason for hiding this comment

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

Other than the .Resolve returning null concern in #1778 (comment) this looks good.

@mateoatr
Copy link
Contributor Author

Other than the .Resolve returning null concern in #1778 (comment) this looks good.

Doesn't

https://github.com/mono/linker/blob/bcc7dec192953e36ef9274c54f7a636d25b02f09/src/linker/Linker.Steps/MarkStep.cs#L277-L278

Solve this?

@vitek-karas
Copy link
Member

Of course it does... sorry - my bad.

Annotations.Mark (method, new DependencyInfo (DependencyKind.MemberOfType, type));
Annotations.SetAction (method, MethodAction.ForceParse);
EnqueueMethod (method, new DependencyInfo (DependencyKind.MemberOfType, type));
MarkMethod (method, new DependencyInfo (reason.Kind == DependencyKind.AccessedViaReflection ? reason.Kind : DependencyKind.MemberOfType, type), new MessageOrigin (reason.Source as IMemberDefinition));
Copy link
Member

Choose a reason for hiding this comment

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

Why the special case for AccessedViaReflection here - is it just to ensure the reason is one of the expected ones in ProcessRequiresUnreferencedCode?

  • Can we do this without changing the reason for the marked method?
  • If we go with the special case, should we do the same for DependencyKind.DynamicDependency - since we can get here from MarkDynamicDependency?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why the special case for AccessedViaReflection here - is it just to ensure the reason is one of the expected ones in ProcessRequiresUnreferencedCode?

Yes.

Can we do this without changing the reason for the marked method?

We can do this if we add DynamicDependency to the list of reasons for marking a method... I think it makes sense and should make code cleaner/easier to read.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure you'd need to add DynamicDependency if we continue passing MemberOfType - I think you'd need to add MemberOfType to the list of cases in ProcessRequiresUnreferencedCode.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just tried not special casing. If we simple reuse reason.Kind then we can end up passing BaseType or NestedType, which then throws, since these are not valid reasons for marking a method.
The reason to special case here is so that we warn on annotated methods on a type that is kept because of DynamicallyAccessedMembers.All. We don't have MembersOfType on the list of reasons to process ProcessRequiresUnreferencedCode since we don't want to emit warnings on methods that are marked because of a copied assembly (we only want to warn if there's a possibility of the method being referenced through reflection or if it's directly called).
I think it's easier to just keep this special case and of course, special case DynamicDependency too.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, that makes sense, to avoid warnings for copy assemblies. Thanks for the explanation!

Just another thought - now I am wondering if we could use sourceLocationMember only, instead of DependencyInfo, for this purpose - since DependencyInfo was designed to be more granular than source locations, and what we need here are source locations. But that might require more changes to the sourceLocationMember logic - I'm not sure. I'll leave it as a suggestion in case you think it's worth looking into, but I think special-casing AccessedViaReflection/DynamicDependency is fine too.

// to be marked. This is important to avoid spurious warnings as virtual overrides can end up being
// marked without being referenced directly from user code. It is important to pass the base method
// here so that MarkMethod can see that it was marked because of an override.
MarkMethod (method, new DependencyInfo (DependencyKind.OverrideOnInstantiatedType, method.DeclaringType), new MessageOrigin (@base));
Copy link
Member

@sbomer sbomer Jan 29, 2021

Choose a reason for hiding this comment

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

Is this logic still needed to avoid spurious warnings? It looks to me like it's not, since ProcessRequiresUnreferencedCode doesn't have a case for OverrideOnInstantiatedType (or maybe it's missing a case?)

More generally (not directly related to your change) - I'm not sure what the function of sourceLocationMember is here - I think we should just pass null. If I'm not missing something, it looks like:

  • At this point, method and @base are MethodDefinitions
  • MarkMethod will only use sourceLocationMember for instantiated generic types or methods (the latter in GetOriginalMethod)

I think the sourceLocationMember is only used for instantiated generics, but can't have instantiated generics here, so we shouldn't use it. Does that sound right to you? @vitek-karas I think you introduced these - what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right it's no longer needed; when calling MarkMethod from here we will never emit a warning.

More generally (not directly related to your change) - I'm not sure what the function of sourceLocationMember is here - I think we should just pass null.

Yes, since there's no way we will emit a warning in MarkMethod at this point, we can pass null here. Other than helping us report the warning, sourceLocationMember becomes relevant in the context of MarkMethod when dealing with generics, as you noted (we could end here:

https://github.com/mono/linker/blob/ba990ce8336dd278b31007aac30ac85f120d3683/src/linker/Linker.Steps/MarkStep.cs#L2267

I'll change the origin here to null, unless @vitek-karas disagrees / there a reason to pass @base/method.DeclaringType here.

Copy link
Member

@sbomer sbomer left a comment

Choose a reason for hiding this comment

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

LGTM otherwise!

Pass null to origin's parameter when processing optimized overrides
Add DynamicDependency to list of valid dependency kinds in ProcessRequiresUnreferencedCode
@mateoatr mateoatr merged commit 6745019 into dotnet:master Feb 1, 2021
agocke pushed a commit to dotnet/runtime that referenced this pull request Nov 16, 2022
…t/linker#1778)

* Only process direct calls and reflection method references.

* Clean test

* Add tests

* Check for null origin

* Warn on DynamicallyAccessedMember kind.

* Update warnings message for Requires Unreferenced Code

* Mark interfaces of types accessed via reflection

* Fix whitespace

* Add includeInterfaceTypes parameter

* Keep set action for methods in MarkEntireType

* Add test for dynamic dependency with RUC.

* Fix DependencyKind in MarkTypeForDynamicallyAccessedMembers
Pass null to origin's parameter when processing optimized overrides
Add DynamicDependency to list of valid dependency kinds in ProcessRequiresUnreferencedCode

* Fix formatting

Commit migrated from dotnet/linker@6745019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants