Warn on called methods annotated with RequiresUnreferencedCode#1778
Warn on called methods annotated with RequiresUnreferencedCode#1778mateoatr merged 13 commits intodotnet:masterfrom
Conversation
f895748 to
e7af1b8
Compare
test/Mono.Linker.Tests.Cases/RequiresCapability/RequiresUnreferencedCodeCapability.cs
Outdated
Show resolved
Hide resolved
test/Mono.Linker.Tests.Cases/RequiresCapability/RequiresUnreferencedCodeCapability.cs
Show resolved
Hide resolved
| } | ||
|
|
||
| [ExpectedWarning ("IL2026", "--MethodCalledThroughReflection--")] | ||
| static void TestRequiresThroughReflectionInMethodFromCopiedAssembly () |
There was a problem hiding this comment.
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));
src/linker/Linker.Steps/MarkStep.cs
Outdated
| } | ||
| } | ||
|
|
||
| DependencyInfo GetDependencyInfo (IMemberDefinition member, DependencyInfo reason) |
There was a problem hiding this comment.
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.
src/linker/Linker.Steps/MarkStep.cs
Outdated
| foreach (InterfaceImplementation iface in type.Interfaces) { | ||
| MarkInterfaceImplementation (iface, type); | ||
| if (reason.Kind == DependencyKind.AccessedViaReflection) | ||
| MarkEntireTypeInternal (iface.InterfaceType.Resolve (), includeBaseTypes: true, reason, type, typesAlreadyVisited); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Also - it's technically possible for Resole to return null, the should not crash on it. See handling of base types.
src/linker/Linker.Steps/MarkStep.cs
Outdated
| if (reason.Kind == DependencyKind.AccessedViaReflection) | ||
| ProcessRequiresUnreferencedCode (method, new MessageOrigin (reason.Source as IMemberDefinition), reason.Kind); |
There was a problem hiding this comment.
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.
src/linker/Linker.Steps/MarkStep.cs
Outdated
| 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)); |
There was a problem hiding this comment.
We should keep the SetAction - the difference in behavior will be pretty small, but still - no reason to NOT do that.
vitek-karas
left a comment
There was a problem hiding this comment.
Other than the .Resolve returning null concern in #1778 (comment) this looks good.
Doesn't Solve this? |
|
Of course it does... sorry - my bad. |
src/linker/Linker.Steps/MarkStep.cs
Outdated
| 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)); |
There was a problem hiding this comment.
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 fromMarkDynamicDependency?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/linker/Linker.Steps/MarkStep.cs
Outdated
| // 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)); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
I'll change the origin here to null, unless @vitek-karas disagrees / there a reason to pass @base/method.DeclaringType here.
Pass null to origin's parameter when processing optimized overrides Add DynamicDependency to list of valid dependency kinds in ProcessRequiresUnreferencedCode
…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
See #1607. Most of the changes in this PR were already in #1639. We only run through
ProcessRequiresUnreferencedCodewhenever 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).