From b6609f25fad5abb834550256eb46f23348d90e29 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Jul 2026 18:53:07 +0000 Subject: [PATCH 1/8] Initial plan From 860048e3fa9c978bfa8662a8b585fb55e378d181 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:44:54 +0000 Subject: [PATCH 2/8] Add ILLink unused annotation regression coverage Co-authored-by: jtschuster <36744439+jtschuster@users.noreply.github.com> --- .../UnusedVirtualMethodAnnotations.cs | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs new file mode 100644 index 00000000000000..9137a74c34d030 --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs @@ -0,0 +1,84 @@ +// 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; +using System.Diagnostics.CodeAnalysis; +using Mono.Linker.Tests.Cases.Expectations.Assertions; +using Mono.Linker.Tests.Cases.Expectations.Helpers; + +namespace Mono.Linker.Tests.Cases.DataFlow +{ + [SkipKeptItemsValidation] + [ExpectedNoWarnings] + class UnusedVirtualMethodAnnotations + { + [UnconditionalSuppressMessage("Test", "IL2026")] + public static void Main() + { + _ = typeof(TypeOnlyImplementation); + + IUsed used = new UsedImplementation(); + used.Method(typeof(object)); + + IPartiallyUsed partiallyUsed = new PartiallyUsedImplementation(); + partiallyUsed.Method(typeof(object)); + } + + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] + interface IUnused + { + [RequiresUnreferencedCode(nameof(Method))] + void Method([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type); + } + + class UnusedImplementation : IUnused + { + public void Method(Type type) { } + } + + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] + interface ITypeOnly + { + [RequiresUnreferencedCode(nameof(Method))] + void Method([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type); + } + + class TypeOnlyImplementation : ITypeOnly + { + public void Method(Type type) { } + } + + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] + interface IUsed + { + [RequiresUnreferencedCode(nameof(Method))] + void Method([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type); + } + + class UsedImplementation : IUsed + { + [ExpectedWarning("IL2046")] + [ExpectedWarning("IL2092")] + public void Method(Type type) { } + } + + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] + interface IPartiallyUsed + { + [RequiresUnreferencedCode(nameof(Method))] + void Method([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type); + } + + class PartiallyUsedImplementation : IPartiallyUsed + { + [ExpectedWarning("IL2046")] + [ExpectedWarning("IL2092")] + public void Method(Type type) { } + } + + class UnusedImplementationOfPartiallyUsedInterface : IPartiallyUsed + { + public void Method(Type type) { } + } + } +} From c60ab0a232783a7382b51637b5dcc89ed7bb5690 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Jul 2026 20:25:12 +0000 Subject: [PATCH 3/8] Add UnexpectedWarning annotations for analyzer-only IL2046/IL2092 on unused implementations Co-authored-by: jtschuster <36744439+jtschuster@users.noreply.github.com> --- .../DataFlow/UnusedVirtualMethodAnnotations.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs index 9137a74c34d030..24e4fe1706e5ea 100644 --- a/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs @@ -33,6 +33,8 @@ interface IUnused class UnusedImplementation : IUnused { + [UnexpectedWarning("IL2046", Tool.Analyzer, "https://github.com/dotnet/runtime/pull/130640")] + [UnexpectedWarning("IL2092", Tool.Analyzer, "https://github.com/dotnet/runtime/pull/130640")] public void Method(Type type) { } } @@ -45,6 +47,8 @@ interface ITypeOnly class TypeOnlyImplementation : ITypeOnly { + [UnexpectedWarning("IL2046", Tool.Analyzer, "https://github.com/dotnet/runtime/pull/130640")] + [UnexpectedWarning("IL2092", Tool.Analyzer, "https://github.com/dotnet/runtime/pull/130640")] public void Method(Type type) { } } @@ -78,6 +82,8 @@ public void Method(Type type) { } class UnusedImplementationOfPartiallyUsedInterface : IPartiallyUsed { + [UnexpectedWarning("IL2046", Tool.Analyzer, "https://github.com/dotnet/runtime/pull/130640")] + [UnexpectedWarning("IL2092", Tool.Analyzer, "https://github.com/dotnet/runtime/pull/130640")] public void Method(Type type) { } } } From 51884f21d6fac2ae7d9a71bcb3006c2fff049ead Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Jul 2026 20:35:19 +0000 Subject: [PATCH 4/8] Update UnexpectedWarning reason strings to 'Analyzer does not track reachability' Co-authored-by: jtschuster <36744439+jtschuster@users.noreply.github.com> --- .../DataFlow/UnusedVirtualMethodAnnotations.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs index 24e4fe1706e5ea..81095ecfd7cea7 100644 --- a/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs @@ -33,8 +33,8 @@ interface IUnused class UnusedImplementation : IUnused { - [UnexpectedWarning("IL2046", Tool.Analyzer, "https://github.com/dotnet/runtime/pull/130640")] - [UnexpectedWarning("IL2092", Tool.Analyzer, "https://github.com/dotnet/runtime/pull/130640")] + [UnexpectedWarning("IL2046", Tool.Analyzer, "Analyzer does not track reachability")] + [UnexpectedWarning("IL2092", Tool.Analyzer, "Analyzer does not track reachability")] public void Method(Type type) { } } @@ -47,8 +47,8 @@ interface ITypeOnly class TypeOnlyImplementation : ITypeOnly { - [UnexpectedWarning("IL2046", Tool.Analyzer, "https://github.com/dotnet/runtime/pull/130640")] - [UnexpectedWarning("IL2092", Tool.Analyzer, "https://github.com/dotnet/runtime/pull/130640")] + [UnexpectedWarning("IL2046", Tool.Analyzer, "Analyzer does not track reachability")] + [UnexpectedWarning("IL2092", Tool.Analyzer, "Analyzer does not track reachability")] public void Method(Type type) { } } @@ -82,8 +82,8 @@ public void Method(Type type) { } class UnusedImplementationOfPartiallyUsedInterface : IPartiallyUsed { - [UnexpectedWarning("IL2046", Tool.Analyzer, "https://github.com/dotnet/runtime/pull/130640")] - [UnexpectedWarning("IL2092", Tool.Analyzer, "https://github.com/dotnet/runtime/pull/130640")] + [UnexpectedWarning("IL2046", Tool.Analyzer, "Analyzer does not track reachability")] + [UnexpectedWarning("IL2092", Tool.Analyzer, "Analyzer does not track reachability")] public void Method(Type type) { } } } From 5bc07dcb888b41c0efe1010a7691628a8faab8b0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Jul 2026 15:59:01 +0000 Subject: [PATCH 5/8] Change UnexpectedWarning to ExpectedWarning for analyzer-only IL2046/IL2092 warnings Co-authored-by: sbomer <787361+sbomer@users.noreply.github.com> --- .../DataFlow/UnusedVirtualMethodAnnotations.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs index 81095ecfd7cea7..a0540de6e193ba 100644 --- a/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs @@ -33,8 +33,8 @@ interface IUnused class UnusedImplementation : IUnused { - [UnexpectedWarning("IL2046", Tool.Analyzer, "Analyzer does not track reachability")] - [UnexpectedWarning("IL2092", Tool.Analyzer, "Analyzer does not track reachability")] + [ExpectedWarning("IL2046", Tool.Analyzer, "Analyzer does not track reachability")] + [ExpectedWarning("IL2092", Tool.Analyzer, "Analyzer does not track reachability")] public void Method(Type type) { } } @@ -47,8 +47,8 @@ interface ITypeOnly class TypeOnlyImplementation : ITypeOnly { - [UnexpectedWarning("IL2046", Tool.Analyzer, "Analyzer does not track reachability")] - [UnexpectedWarning("IL2092", Tool.Analyzer, "Analyzer does not track reachability")] + [ExpectedWarning("IL2046", Tool.Analyzer, "Analyzer does not track reachability")] + [ExpectedWarning("IL2092", Tool.Analyzer, "Analyzer does not track reachability")] public void Method(Type type) { } } @@ -82,8 +82,8 @@ public void Method(Type type) { } class UnusedImplementationOfPartiallyUsedInterface : IPartiallyUsed { - [UnexpectedWarning("IL2046", Tool.Analyzer, "Analyzer does not track reachability")] - [UnexpectedWarning("IL2092", Tool.Analyzer, "Analyzer does not track reachability")] + [ExpectedWarning("IL2046", Tool.Analyzer, "Analyzer does not track reachability")] + [ExpectedWarning("IL2092", Tool.Analyzer, "Analyzer does not track reachability")] public void Method(Type type) { } } } From ecf57b249b8da8d5be4ac3e7acafdf81298d4043 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Jul 2026 18:59:49 +0000 Subject: [PATCH 6/8] Remove DAM from interface type declarations in UnusedVirtualMethodAnnotations test Co-authored-by: jtschuster <36744439+jtschuster@users.noreply.github.com> --- .../DataFlow/UnusedVirtualMethodAnnotations.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs index a0540de6e193ba..08772a45d4e532 100644 --- a/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs @@ -24,7 +24,6 @@ public static void Main() partiallyUsed.Method(typeof(object)); } - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] interface IUnused { [RequiresUnreferencedCode(nameof(Method))] @@ -38,7 +37,6 @@ class UnusedImplementation : IUnused public void Method(Type type) { } } - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] interface ITypeOnly { [RequiresUnreferencedCode(nameof(Method))] @@ -52,7 +50,6 @@ class TypeOnlyImplementation : ITypeOnly public void Method(Type type) { } } - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] interface IUsed { [RequiresUnreferencedCode(nameof(Method))] @@ -66,7 +63,6 @@ class UsedImplementation : IUsed public void Method(Type type) { } } - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] interface IPartiallyUsed { [RequiresUnreferencedCode(nameof(Method))] From 77071cd0f5245e7f32a8cd6fa978afc7d85a8e45 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:20:08 -0700 Subject: [PATCH 7/8] Match warnings reported on members the trimmer removed Validation runs before SweepStep, so a warning can be reported on a member that is later removed. Removing a member detaches it from its declaring type, so Cecil renders its FullName without the namespace or declaring type. ResultChecker reads that name after the pipeline finishes, so nothing matches: [ExpectedWarning] reports the warning as missing, and [ExpectedNoWarnings] silently skips it. Record the origin member's names in TrimmingTestLogger while the member is still attached, and prefer those names when matching in ResultChecker. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3335d6f1-a5e0-4e7a-895b-1ee3232c46ed --- .../TestCasesRunner/ResultChecker.cs | 41 ++++++++++++++----- .../TestCasesRunner/TrimmingTestLogger.cs | 35 ++++++++++++++++ 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs b/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs index 4663dcae82c15c..e7dbad5bc01eed 100644 --- a/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs +++ b/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs @@ -915,6 +915,12 @@ void VerifyLoggedMessages(AssemblyDefinition original, TrimmingTestLogger logger List<(ICustomAttributeProvider, CustomAttribute)> expectedNoWarningsAttributes = new(); List missingMessageWarnings = []; List unexpectedMessageWarnings = []; + + // A warning may be reported on a member which the trimmer then removed. Cecil detaches a + // removed member from its declaring type, so reading the member's names now would give a + // FullName without the namespace or declaring type, which no member of the original + // assembly matches. Use the names the logger recorded when the message was logged. + TrimmingTestLogger.OriginNames? OriginNames(MessageContainer message) => logger.GetOriginNames(message); foreach (var attrProvider in GetAttributeProviders(original)) { if (attrProvider is MethodDefinition attrMethod && @@ -1070,9 +1076,14 @@ attrMethod.DeclaringType is TypeDefinition declaringType && if (loggedMessage.Origin?.Provider is not IMemberDefinition memberDefinition) continue; + var originNames = OriginNames(loggedMessage); + string actualMemberName = originNames?.Name ?? memberDefinition.Name; + string actualDeclaringTypeFullName = originNames?.DeclaringTypeFullName ?? memberDefinition.DeclaringType?.FullName; + string actualDeclaringTypeName = originNames?.DeclaringTypeName ?? memberDefinition.DeclaringType?.Name; + if (attrProvider is IMemberDefinition expectedMember) { - string actualName = memberDefinition.DeclaringType.FullName + "." + memberDefinition.Name; + string actualName = actualDeclaringTypeFullName + "." + actualMemberName; if (actualName.StartsWith(expectedMember.DeclaringType.FullName) && (actualName.Contains("<" + expectedMember.Name + ">") || @@ -1087,21 +1098,21 @@ attrMethod.DeclaringType is TypeDefinition declaringType && continue; if (actualName.StartsWith(expectedMember.DeclaringType.FullName)) { - if (memberDefinition.Name == ".cctor" && + if (actualMemberName == ".cctor" && (expectedMember is FieldDefinition || expectedMember is PropertyDefinition)) { expectedWarningFound = true; unmatchedMessages.Remove(loggedMessage); break; } - if (memberDefinition.Name == ".ctor" && - (expectedMember is FieldDefinition || expectedMember is PropertyDefinition || memberDefinition.DeclaringType.FullName == expectedMember.FullName)) + if (actualMemberName == ".ctor" && + (expectedMember is FieldDefinition || expectedMember is PropertyDefinition || actualDeclaringTypeFullName == expectedMember.FullName)) { expectedWarningFound = true; unmatchedMessages.Remove(loggedMessage); break; } - if (memberDefinition.DeclaringType.Name.StartsWith("") && memberDefinition.Name == expectedMember.Name) + if (actualDeclaringTypeName?.StartsWith("") == true && actualMemberName == expectedMember.Name) { expectedWarningFound = true; unmatchedMessages.Remove(loggedMessage); @@ -1112,8 +1123,8 @@ attrMethod.DeclaringType is TypeDefinition declaringType && else if (attrProvider is AssemblyDefinition expectedAssembly) { // Allow assembly-level attributes to match warnings from compiler-generated Main - if (memberDefinition.Name == "
$" && - memberDefinition.DeclaringType.FullName == "Program" && + if (actualMemberName == "
$" && + actualDeclaringTypeFullName == "Program" && memberDefinition.DeclaringType.Module.Assembly.Name.Name == expectedAssembly.Name.Name) { expectedWarningFound = true; @@ -1187,7 +1198,8 @@ attrMethod.DeclaringType is TypeDefinition declaringType && continue; // This is a hacky way to say anything in the "subtree" of the attrProvider - if (attrProvider is IMemberDefinition attrMember && (mc.Origin?.Provider is IMemberDefinition member) && member.FullName.Contains(attrMember.FullName) != true) + if (attrProvider is IMemberDefinition attrMember && (mc.Origin?.Provider is IMemberDefinition member) + && (OriginNames(mc)?.FullName ?? member.FullName).Contains(attrMember.FullName) != true) continue; unexpectedMessageWarnings.Add($"Unexpected warning found: {mc}"); @@ -1197,7 +1209,16 @@ attrMethod.DeclaringType is TypeDefinition declaringType && if (missingMessageWarnings.Any()) { missingMessageWarnings.Add("Unmatched Messages:" + Environment.NewLine); - missingMessageWarnings.AddRange(unmatchedMessages.Select(m => m.ToString())); + missingMessageWarnings.AddRange(unmatchedMessages.Select(m => + { + // A message whose origin member was removed by the trimmer renders without its + // namespace or declaring type, so also show the name the member had when the + // message was logged. + var names = OriginNames(m); + return names is null || m.ToString().Contains(names.Value.DeclaringTypeFullName ?? string.Empty) + ? m.ToString() + : $"{m} (origin was removed by the trimmer: {names.Value.FullName})"; + })); // Uncomment to show all messages when diagnosing test infrastructure issues // missingMessageWarnings.Add(Environment.NewLine + "All Messages:" + Environment.NewLine); // missingMessageWarnings.AddRange(allMessages.Select(m => m.ToString())); @@ -1228,7 +1249,7 @@ bool LogMessageHasSameOriginMember(MessageContainer mc, ICustomAttributeProvider if (expectedOriginProvider is not IMemberDefinition expectedOriginMember) return false; - return actualMember.FullName == expectedOriginMember.FullName; + return (OriginNames(mc)?.FullName ?? actualMember.FullName) == expectedOriginMember.FullName; } } diff --git a/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/TrimmingTestLogger.cs b/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/TrimmingTestLogger.cs index c129822af85447..f7ff22ecae92e3 100644 --- a/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/TrimmingTestLogger.cs +++ b/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/TrimmingTestLogger.cs @@ -4,16 +4,26 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using Mono.Cecil; namespace Mono.Linker.Tests.TestCasesRunner { public class TrimmingTestLogger : ILogger { + /// + /// The names of a warning's origin member, captured while the member is still attached to + /// its declaring type. + /// + public readonly record struct OriginNames(string FullName, string DeclaringTypeFullName, string DeclaringTypeName, string Name); + readonly List MessageContainers; + readonly Dictionary OriginNamesByProvider; + public TrimmingTestLogger() { MessageContainers = new List(); + OriginNamesByProvider = new Dictionary(ReferenceEqualityComparer.Instance); } public ImmutableArray GetLoggedMessages() @@ -21,6 +31,18 @@ public ImmutableArray GetLoggedMessages() return MessageContainers.ToImmutableArray(); } + /// + /// Returns the names the origin member of had when the message was + /// logged, or null if the message has no member origin. + /// + public OriginNames? GetOriginNames(MessageContainer message) + { + if (message.Origin?.Provider is not object provider) + return null; + + return OriginNamesByProvider.TryGetValue(provider, out var names) ? names : null; + } + public void LogMessage(MessageContainer message) { // This is to force Cecil to load all the information from the assembly @@ -28,6 +50,19 @@ public void LogMessage(MessageContainer message) // later on during validation, it may already be closed and Cecil's lazy loading might fail. message.ToString(); + // Warnings can be reported on members which the trimmer goes on to remove. Removing a + // member detaches it from its declaring type, so afterwards its FullName no longer + // includes the namespace or the declaring type and can't be matched against the names + // the test's expectations refer to. Record the names while they are still accurate. + if (message.Origin?.Provider is IMemberDefinition member && !OriginNamesByProvider.ContainsKey(member)) + { + OriginNamesByProvider.Add(member, new OriginNames( + member.FullName, + member.DeclaringType?.FullName, + member.DeclaringType?.Name, + member.Name)); + } + MessageContainers.Add(message); } } From c8005cf2a348bc9c47c84660484423a729568b7d Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Fri, 31 Jul 2026 11:20:18 -0700 Subject: [PATCH 8/8] Add cases where the trimmer warns on unused overrides The existing cases declare the interface in the assembly being linked and never mark it, so MarkStep never queues the base method for validation and nothing warns. To reproduce #98870 the base member has to come from an assembly with AssemblyAction != Link (corelib in a real app), or be marked by another implementation. Adds: - an implementation of an interface marked by a different implementation - implementations of interfaces in a copy-action assembly, both unreferenced and referenced only through typeof - an override of an abstract class in a copy-action assembly - an override of a System.Type-derived class, where the annotation applies to the implicit 'this' parameter and the mismatch is IL2094 rather than IL2092 The trimmer warnings use [UnexpectedWarning] since they should go away once #98870 is fixed. The analyzer warns on all of these by design, as it has no reachability analysis, so those stay [ExpectedWarning]. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3335d6f1-a5e0-4e7a-895b-1ee3232c46ed --- .../ILTrim.Tests/ILTrimExpectedFailures.txt | 1 + .../DataFlowTests.g.cs | 6 ++ .../UnusedVirtualMethodAnnotationsBase.cs | 41 ++++++++++ .../UnusedVirtualMethodAnnotations.cs | 74 +++++++++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/Dependencies/UnusedVirtualMethodAnnotationsBase.cs diff --git a/src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt b/src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt index bb539fdcb8c5fd..d8808d2a556dff 100644 --- a/src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt +++ b/src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt @@ -105,6 +105,7 @@ DataFlow.RuntimeAsyncMethods DataFlow.StaticInterfaceMethodDataflow DataFlow.SuppressWarningWithLinkAttributes DataFlow.UnresolvedMembers +DataFlow.UnusedVirtualMethodAnnotations DataFlow.VirtualMethodHierarchyDataflowAnnotationValidation DataFlow.XmlAnnotations DynamicDependencies.DynamicDependencyField diff --git a/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/generated/ILLink.RoslynAnalyzer.Tests.Generator/ILLink.RoslynAnalyzer.Tests.TestCaseGenerator/DataFlowTests.g.cs b/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/generated/ILLink.RoslynAnalyzer.Tests.Generator/ILLink.RoslynAnalyzer.Tests.TestCaseGenerator/DataFlowTests.g.cs index 1c5d2c6d252e7c..7a7579c792284f 100644 --- a/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/generated/ILLink.RoslynAnalyzer.Tests.Generator/ILLink.RoslynAnalyzer.Tests.TestCaseGenerator/DataFlowTests.g.cs +++ b/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/generated/ILLink.RoslynAnalyzer.Tests.Generator/ILLink.RoslynAnalyzer.Tests.TestCaseGenerator/DataFlowTests.g.cs @@ -67,5 +67,11 @@ public Task UnsafeDataFlow() return RunTest(allowMissingWarnings: true); } + [Fact] + public Task UnusedVirtualMethodAnnotations() + { + return RunTest(allowMissingWarnings: true); + } + } } diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/Dependencies/UnusedVirtualMethodAnnotationsBase.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/Dependencies/UnusedVirtualMethodAnnotationsBase.cs new file mode 100644 index 00000000000000..9a12868bd5caa0 --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/Dependencies/UnusedVirtualMethodAnnotationsBase.cs @@ -0,0 +1,41 @@ +// 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; +using System.Diagnostics.CodeAnalysis; + +namespace Mono.Linker.Tests.Cases.DataFlow.Dependencies +{ + public interface IUnusedInPreservedAssembly + { + [RequiresUnreferencedCode(nameof(Method))] + void Method([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type); + } + + public interface ITypeOnlyInPreservedAssembly + { + [RequiresUnreferencedCode(nameof(Method))] + void Method([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type); + } + + public interface IPartiallyUsedInPreservedAssembly + { + [RequiresUnreferencedCode(nameof(Method))] + void Method([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type); + } + + public abstract class BaseInPreservedAssembly + { + [RequiresUnreferencedCode(nameof(Method))] + public abstract void Method([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type); + } + + // Annotations on methods of a System.Type-derived type apply to the implicit 'this' parameter, + // so a mismatch on an override is reported as IL2094 rather than IL2092. The issue used + // System.Reflection.IReflect, which the trimmer treats the same way. + public abstract class TypeWithAnnotatedThisInPreservedAssembly : Type + { + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] + public virtual void Method() { } + } +} diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs index 08772a45d4e532..64ed36b5e77d8a 100644 --- a/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/DataFlow/UnusedVirtualMethodAnnotations.cs @@ -3,13 +3,17 @@ using System; using System.Diagnostics.CodeAnalysis; +using Mono.Linker.Tests.Cases.DataFlow.Dependencies; using Mono.Linker.Tests.Cases.Expectations.Assertions; using Mono.Linker.Tests.Cases.Expectations.Helpers; +using Mono.Linker.Tests.Cases.Expectations.Metadata; namespace Mono.Linker.Tests.Cases.DataFlow { [SkipKeptItemsValidation] [ExpectedNoWarnings] + [SetupCompileBefore("base.dll", new[] { "Dependencies/UnusedVirtualMethodAnnotationsBase.cs" })] + [SetupLinkerAction("copy", "base")] class UnusedVirtualMethodAnnotations { [UnconditionalSuppressMessage("Test", "IL2026")] @@ -22,6 +26,11 @@ public static void Main() IPartiallyUsed partiallyUsed = new PartiallyUsedImplementation(); partiallyUsed.Method(typeof(object)); + + _ = typeof(TypeOnlyImplementationOfPreservedInterface); + + IPartiallyUsedInPreservedAssembly partiallyUsedFromPreservedAssembly = new UsedImplementationOfPreservedInterface(); + partiallyUsedFromPreservedAssembly.Method(typeof(object)); } interface IUnused @@ -76,11 +85,76 @@ class PartiallyUsedImplementation : IPartiallyUsed public void Method(Type type) { } } + // The trimmer warns about this unused implementation because the interface method was + // marked (by PartiallyUsedImplementation) and validation walks every override of a marked + // virtual method, whether or not the override was kept. class UnusedImplementationOfPartiallyUsedInterface : IPartiallyUsed { [ExpectedWarning("IL2046", Tool.Analyzer, "Analyzer does not track reachability")] + [UnexpectedWarning("IL2046", Tool.Trimmer, "https://github.com/dotnet/runtime/issues/98870")] + [ExpectedWarning("IL2092", Tool.Analyzer, "Analyzer does not track reachability")] + [UnexpectedWarning("IL2092", Tool.Trimmer, "https://github.com/dotnet/runtime/issues/98870")] + public void Method(Type type) { } + } + + // The cases below declare the base member in an assembly the trimmer doesn't link. + // Every virtual method of such an assembly is queued for annotation validation, and each + // of its overrides is then validated, so an implementation warns even when nothing in the + // app can reach it. This is the shape reported in the issue, where the base member is + // System.Reflection.IReflect in the (unlinked) framework. + + class UnusedImplementationOfPreservedInterface : IUnusedInPreservedAssembly + { + [ExpectedWarning("IL2046", Tool.Analyzer, "Analyzer does not track reachability")] + [UnexpectedWarning("IL2046", Tool.Trimmer, "https://github.com/dotnet/runtime/issues/98870")] [ExpectedWarning("IL2092", Tool.Analyzer, "Analyzer does not track reachability")] + [UnexpectedWarning("IL2092", Tool.Trimmer, "https://github.com/dotnet/runtime/issues/98870")] public void Method(Type type) { } } + + class TypeOnlyImplementationOfPreservedInterface : ITypeOnlyInPreservedAssembly + { + [ExpectedWarning("IL2046", Tool.Analyzer, "Analyzer does not track reachability")] + [UnexpectedWarning("IL2046", Tool.Trimmer, "https://github.com/dotnet/runtime/issues/98870")] + [ExpectedWarning("IL2092", Tool.Analyzer, "Analyzer does not track reachability")] + [UnexpectedWarning("IL2092", Tool.Trimmer, "https://github.com/dotnet/runtime/issues/98870")] + public void Method(Type type) { } + } + + class UsedImplementationOfPreservedInterface : IPartiallyUsedInPreservedAssembly + { + [ExpectedWarning("IL2046")] + [ExpectedWarning("IL2092")] + public void Method(Type type) { } + } + + class UnusedImplementationOfUsedPreservedInterface : IPartiallyUsedInPreservedAssembly + { + [ExpectedWarning("IL2046", Tool.Analyzer, "Analyzer does not track reachability")] + [UnexpectedWarning("IL2046", Tool.Trimmer, "https://github.com/dotnet/runtime/issues/98870")] + [ExpectedWarning("IL2092", Tool.Analyzer, "Analyzer does not track reachability")] + [UnexpectedWarning("IL2092", Tool.Trimmer, "https://github.com/dotnet/runtime/issues/98870")] + public void Method(Type type) { } + } + + class UnusedOverrideOfPreservedBaseClass : BaseInPreservedAssembly + { + [ExpectedWarning("IL2046", Tool.Analyzer, "Analyzer does not track reachability")] + [UnexpectedWarning("IL2046", Tool.Trimmer, "https://github.com/dotnet/runtime/issues/98870")] + [ExpectedWarning("IL2092", Tool.Analyzer, "Analyzer does not track reachability")] + [UnexpectedWarning("IL2092", Tool.Trimmer, "https://github.com/dotnet/runtime/issues/98870")] + public override void Method(Type type) { } + } + + // The shape from the issue, where the base member's annotation applies to the implicit + // 'this' parameter so the mismatch is reported as IL2094 rather than IL2092. Only + // System.Type and System.Reflection.IReflect hierarchies annotate 'this'; the issue used + // IReflect, and deriving from System.Type covers the same validation path. + abstract class UnusedOverrideOfPreservedTypeWithAnnotatedThis : TypeWithAnnotatedThisInPreservedAssembly + { + [ExpectedWarning("IL2094", Tool.Analyzer, "Analyzer does not track reachability")] + [UnexpectedWarning("IL2094", Tool.Trimmer, "https://github.com/dotnet/runtime/issues/98870")] + public override void Method() { } + } } }