From 530179d1dcf3180cc79878cdac137611d5dcacd6 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 7 Oct 2025 11:24:15 -0700 Subject: [PATCH 1/7] Update TypeMap attribute handling - Mark TypeMapAssemblyTargetAttributes when a typemapuniverse is marked - Recurse through TypeMapAssemblyTarget assemblies to find all TypeMapAttributes - Refactor TypeMapHandler construction and initialization - Use TypeReferenceEqualityComparer for Dictionaries - Add more test coverage --- .../src/linker/Linker.Steps/MarkStep.cs | 11 +- .../src/linker/Linker.Steps/SweepStep.cs | 2 +- .../src/linker/Linker/TypeMapHandler.cs | 247 +++++++++++------- .../Linker/TypeReferenceEqualityComparer.cs | 3 + .../Dependencies/TypeMapReferencedAssembly.cs | 51 ++++ .../TypeMapSecondOrderReference.cs | 43 +++ .../Reflection/TypeMap.cs | 166 ++++++++---- 7 files changed, 363 insertions(+), 160 deletions(-) create mode 100644 src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapReferencedAssembly.cs create mode 100644 src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapSecondOrderReference.cs diff --git a/src/tools/illink/src/linker/Linker.Steps/MarkStep.cs b/src/tools/illink/src/linker/Linker.Steps/MarkStep.cs index 9ae7f7b9cea5c9..8c4bcd08ff4e63 100644 --- a/src/tools/illink/src/linker/Linker.Steps/MarkStep.cs +++ b/src/tools/illink/src/linker/Linker.Steps/MarkStep.cs @@ -254,14 +254,7 @@ protected virtual void Initialize() { InitializeCorelibAttributeXml(); Context.Pipeline.InitializeMarkHandlers(Context, MarkContext); - - if (Annotations.GetEntryPointAssembly() is AssemblyDefinition entryPoint) - { - _typeMapHandler = new TypeMapHandler(entryPoint); - } - - _typeMapHandler.Initialize(Context, this); - + _typeMapHandler.Initialize(Context, this, Annotations.GetEntryPointAssembly()); ProcessMarkedPending(); } @@ -1458,7 +1451,7 @@ protected bool CheckProcessed(IMetadataTokenProvider provider) return !Annotations.SetProcessed(provider); } - protected virtual void MarkAssembly(AssemblyDefinition assembly, DependencyInfo reason, MessageOrigin origin) + public void MarkAssembly(AssemblyDefinition assembly, DependencyInfo reason, MessageOrigin origin) { Annotations.Mark(assembly, reason, origin); if (CheckProcessed(assembly)) diff --git a/src/tools/illink/src/linker/Linker.Steps/SweepStep.cs b/src/tools/illink/src/linker/Linker.Steps/SweepStep.cs index 5ad9a2d337d9db..3a98dadfd2e1ce 100644 --- a/src/tools/illink/src/linker/Linker.Steps/SweepStep.cs +++ b/src/tools/illink/src/linker/Linker.Steps/SweepStep.cs @@ -261,7 +261,7 @@ protected virtual void SweepAssembly(AssemblyDefinition assembly) bool IsMarkedAssembly(AssemblyDefinition assembly) { - return Annotations.IsMarked(assembly.MainModule); + return Annotations.IsMarked(assembly) || Annotations.IsMarked(assembly.MainModule); } bool CanSweepNamesForMember(IMemberDefinition member, MetadataTrimming metadataTrimming) diff --git a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs index 8cbc8fb301f37f..708e7c1a4f1499 100644 --- a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs +++ b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs @@ -4,10 +4,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Text; -using ILLink.Shared.TrimAnalysis; +using System.Linq; using Mono.Cecil; -using Mono.CompilerServices.SymbolWriter; using Mono.Linker.Steps; using CustomAttributeWithOrigin = (Mono.Cecil.CustomAttribute Attribute, Mono.Cecil.AssemblyDefinition Origin); @@ -16,7 +14,9 @@ namespace Mono.Linker { sealed class TypeMapHandler { - readonly TypeMapResolver _lazyTypeMapResolver; + TypeMapResolver _lazyTypeMapResolver = null!; + LinkContext _context = null!; + MarkStep _markStep = null!; // [trim target: [type map group: custom attributes with assembly origin]] readonly Dictionary>> _unmarkedExternalTypeMapEntries = []; @@ -26,85 +26,85 @@ sealed class TypeMapHandler // CustomAttributes that we want to mark when the type mapping APIs are used. // [type map group: custom attributes] - Dictionary> _pendingExternalTypeMapEntries = []; - Dictionary> _pendingProxyTypeMapEntries = []; - HashSet _referencedExternalTypeMaps = []; - HashSet _referencedProxyTypeMaps = []; + Dictionary> _pendingExternalTypeMapEntries = null!; + Dictionary> _pendingProxyTypeMapEntries = null!; + Dictionary> _pendingAssemblyTargets = null!; - LinkContext _context = null!; - MarkStep _markStep = null!; + HashSet _referencedExternalTypeMaps = null!; + HashSet _referencedProxyTypeMaps = null!; public TypeMapHandler() { - _lazyTypeMapResolver = new TypeMapResolver(new HashSet()); } - public TypeMapHandler(AssemblyDefinition entryPointAssembly) + [Conditional("DEBUG")] + private void EnsureInitialized() { - HashSet assemblies = [AssemblyNameReference.Parse(entryPointAssembly.FullName)]; - foreach (var attr in entryPointAssembly.CustomAttributes) - { - if (attr.AttributeType is not GenericInstanceType - { - Namespace: "System.Runtime.InteropServices", - GenericArguments: [_] - }) - { - continue; // Only interested in System.Runtime.InteropServices attributes - } - - if (attr.AttributeType.Name != "TypeMapAssemblyTarget`1" - || attr.ConstructorArguments[0].Value is not string str) - { - // Invalid attribute, skip it. - // Let the runtime handle the failure. - continue; - } - - assemblies.Add(AssemblyNameReference.Parse(str)); - } - - _lazyTypeMapResolver = new TypeMapResolver(assemblies); + if (_lazyTypeMapResolver is null) + throw new InvalidOperationException("TypeMapHandler not initialized"); } - public void Initialize(LinkContext context, MarkStep markStep) + public void Initialize(LinkContext context, MarkStep markStep, AssemblyDefinition? entryPointAssembly) { _context = context; _markStep = markStep; - _lazyTypeMapResolver.Resolve(context, this); + var typeReferenceEqualityComparer = new TypeReferenceEqualityComparer(context); + _pendingExternalTypeMapEntries = new(typeReferenceEqualityComparer); + _pendingProxyTypeMapEntries = new(typeReferenceEqualityComparer); + _pendingAssemblyTargets = new(typeReferenceEqualityComparer); + _referencedExternalTypeMaps = new(typeReferenceEqualityComparer); + _referencedProxyTypeMaps = new(typeReferenceEqualityComparer); + var typeMapResolver = new TypeMapResolver(entryPointAssembly); + typeMapResolver.Resolve(context, this); + _lazyTypeMapResolver = typeMapResolver; } public void ProcessExternalTypeMapGroupSeen(MethodDefinition callingMethod, TypeReference typeMapGroup) { + EnsureInitialized(); _referencedExternalTypeMaps.Add(typeMapGroup); - if (!_pendingExternalTypeMapEntries.Remove(typeMapGroup, out List? pendingEntries)) + if (_pendingExternalTypeMapEntries.Remove(typeMapGroup, out List? pendingEntries)) { - return; + foreach (var entry in pendingEntries) + { + MarkTypeMapAttribute(entry, new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod)); + } } - - foreach (var entry in pendingEntries) + if (_pendingAssemblyTargets.Remove(typeMapGroup, out List? assemblyTargets)) { - MarkTypeMapAttribute(entry, new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod)); + foreach (var entry in assemblyTargets) + { + _markStep.MarkCustomAttribute(entry.Attribute, new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod), new MessageOrigin(entry.Origin)); + _markStep.MarkAssembly(entry.Origin, info, new MessageOrigin(entry.Origin)); + } } } public void ProcessProxyTypeMapGroupSeen(MethodDefinition callingMethod, TypeReference typeMapGroup) { + EnsureInitialized(); _referencedProxyTypeMaps.Add(typeMapGroup); - if (!_pendingProxyTypeMapEntries.Remove(typeMapGroup, out List? pendingEntries)) + if (_pendingProxyTypeMapEntries.Remove(typeMapGroup, out List? pendingEntries)) { - return; + foreach (var entry in pendingEntries) + { + MarkTypeMapAttribute(entry, new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod)); + } } - - foreach (var entry in pendingEntries) + if (_pendingAssemblyTargets.Remove(typeMapGroup, out List? assemblyTargets)) { - MarkTypeMapAttribute(entry, new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod)); + foreach (var entry in assemblyTargets) + { + _markStep.MarkCustomAttribute(entry.Attribute, new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod), new MessageOrigin(entry.Origin)); + _markStep.MarkAssembly(entry.Origin, info, new MessageOrigin(entry.Origin)); + } } } void MarkTypeMapAttribute(CustomAttributeWithOrigin entry, DependencyInfo info) { _markStep.MarkCustomAttribute(entry.Attribute, info, new MessageOrigin(entry.Origin)); + _markStep.MarkAssembly(entry.Origin, info, new MessageOrigin(entry.Origin)); // Mark the target type as instantiated TypeReference targetType = (TypeReference)entry.Attribute.ConstructorArguments[1].Value; @@ -114,6 +114,7 @@ void MarkTypeMapAttribute(CustomAttributeWithOrigin entry, DependencyInfo info) public void ProcessType(TypeDefinition definition) { + EnsureInitialized(); RecordTargetTypeSeen(definition, _unmarkedExternalTypeMapEntries, _referencedExternalTypeMaps, _pendingExternalTypeMapEntries); } @@ -123,32 +124,32 @@ void RecordTargetTypeSeen( HashSet referenceTypeMapGroups, Dictionary> typeMapAttributesPendingUniverseMarking) { - if (unmarkedTypeMapAttributes.Remove(targetType, out Dictionary>? entries)) + if (!unmarkedTypeMapAttributes.Remove(targetType, out Dictionary>? entries)) + return; + + foreach (var (typeMapGroup, attributes) in entries) { - foreach (var (typeMapGroup, attributes) in entries) + if (referenceTypeMapGroups.Contains(typeMapGroup)) { - - if (referenceTypeMapGroups.Contains(typeMapGroup)) - { - foreach (var attr in attributes) - { - MarkTypeMapAttribute(attr, new DependencyInfo(DependencyKind.TypeMapEntry, targetType)); - } - } - else if (!typeMapAttributesPendingUniverseMarking.TryGetValue(typeMapGroup, out List? value)) + foreach (var attr in attributes) { - typeMapAttributesPendingUniverseMarking[typeMapGroup] = [.. attributes]; - } - else - { - value.AddRange(attributes); + MarkTypeMapAttribute(attr, new DependencyInfo(DependencyKind.TypeMapEntry, targetType)); } } + else if (!typeMapAttributesPendingUniverseMarking.TryGetValue(typeMapGroup, out List? value)) + { + typeMapAttributesPendingUniverseMarking[typeMapGroup] = [.. attributes]; + } + else + { + value.AddRange(attributes); + } } } public void ProcessInstantiated(TypeDefinition definition) { + EnsureInitialized(); RecordTargetTypeSeen(definition, _unmarkedProxyTypeMapEntries, _referencedProxyTypeMaps, _pendingProxyTypeMapEntries); } @@ -156,14 +157,12 @@ void AddExternalTypeMapEntry(TypeReference group, CustomAttributeWithOrigin attr { if (attr.Attribute.ConstructorArguments is [_, _, { Value: TypeReference trimTarget }]) { - RecordTypeMapEntry(attr, group, trimTarget, _unmarkedExternalTypeMapEntries); - return; + RecordTypeMapEntry(attr, group, trimTarget, _unmarkedExternalTypeMapEntries, _referencedExternalTypeMaps, _pendingExternalTypeMapEntries); } - if (attr.Attribute.ConstructorArguments is [_, { Value: TypeReference }]) + else if (attr.Attribute.ConstructorArguments is [_, { Value: TypeReference }]) { // There's no trim target, so include the attribute unconditionally. - RecordTypeMapEntry(attr, group, null, _unmarkedExternalTypeMapEntries); - return; + RecordTypeMapEntry(attr, group, null, _unmarkedExternalTypeMapEntries, _referencedExternalTypeMaps, _pendingExternalTypeMapEntries); } // Invalid attribute, skip it. // Let the runtime handle the failure. @@ -174,50 +173,70 @@ void AddProxyTypeMapEntry(TypeReference group, CustomAttributeWithOrigin attr) if (attr.Attribute.ConstructorArguments is [{ Value: TypeReference sourceType }, _]) { // This is a TypeMapAssociationAttribute, which has a single type argument. - RecordTypeMapEntry(attr, group, sourceType, _unmarkedProxyTypeMapEntries); + RecordTypeMapEntry(attr, group, sourceType, _unmarkedProxyTypeMapEntries, _referencedProxyTypeMaps, _pendingProxyTypeMapEntries); return; } // Invalid attribute, skip it. // Let the runtime handle the failure. } - void RecordTypeMapEntry(CustomAttributeWithOrigin attr, TypeReference group, TypeReference? trimTarget, Dictionary>> unmarkedEntryList) + private void AddAssemblyTarget(TypeReference typeMapGroup, CustomAttributeWithOrigin attr) { - if (trimTarget is null) + // Validate attribute + if (attr.Attribute.ConstructorArguments is not ([{ Value: string }])) + return; + + // If the type map group has been seen, mark the attribute immediately + if (_referencedExternalTypeMaps.Contains(typeMapGroup) || _referencedProxyTypeMaps.Contains(typeMapGroup)) { - // If there's no trim target, we can just mark the attribute. - MarkTypeMapAttribute(attr, new DependencyInfo(DependencyKind.TypeMapEntry, null)); + _markStep.MarkCustomAttribute(attr.Attribute, new DependencyInfo(DependencyKind.TypeMapEntry, null), new MessageOrigin(attr.Origin)); return; } - TypeDefinition? typeDef = _context.Resolve(trimTarget); - if (typeDef is null) + // Otherwise, it's pending until the type map group is seen + _pendingAssemblyTargets.AddToList(typeMapGroup, attr); + } + + + void RecordTypeMapEntry(CustomAttributeWithOrigin attr, TypeReference group, TypeReference? dependencySource, Dictionary>> unmarkedEntryList, HashSet seenTypeGroups, Dictionary> pendingEntryList) + { + if (dependencySource is null) + { + // Mark or directly add to pending group + if (seenTypeGroups.Contains(group)) + { + // If there's no trim target, we can just mark the attribute. + MarkTypeMapAttribute(attr, new DependencyInfo(DependencyKind.TypeMapEntry, null)); + return; + } + else + { + pendingEntryList.AddToList(group, attr); + return; + } + } + + TypeDefinition? dependencyTypeDef = _context.Resolve(dependencySource); + if (dependencyTypeDef is null) { return; // Couldn't find the type we were asked about. } - if (_context.Annotations.IsMarked(typeDef)) + if (seenTypeGroups.Contains(group) && attr.DependencySourceRequiresTarget(_context, dependencyTypeDef)) { - MarkTypeMapAttribute(attr, new DependencyInfo(DependencyKind.TypeMapEntry, trimTarget)); + MarkTypeMapAttribute(attr, new DependencyInfo(DependencyKind.TypeMapEntry, dependencySource)); } else { - if (!unmarkedEntryList.TryGetValue(typeDef, out Dictionary>? entries)) + if (!unmarkedEntryList.TryGetValue(dependencyTypeDef, out Dictionary>? entries)) { - entries = new() { + entries = new(new TypeReferenceEqualityComparer(_context)) { { group, [] } }; - unmarkedEntryList[typeDef] = entries; + unmarkedEntryList[dependencyTypeDef] = entries; } - if (!entries.TryGetValue(group, out List? attrs)) - { - entries[group] = [attr]; - } - else - { - attrs.Add(attr); - } + entries.AddToList(group, attr); } } @@ -226,18 +245,18 @@ public static bool IsTypeMapAttributeType(TypeDefinition type) return type is { Namespace: "System.Runtime.InteropServices", Name: "TypeMapAttribute`1" or "TypeMapAssociationAttribute`1" or "TypeMapAssemblyTargetAttribute`1" }; } - class TypeMapResolver(IReadOnlySet assemblies) + class TypeMapResolver(AssemblyDefinition? _assembly) { public void Resolve(LinkContext context, TypeMapHandler manager) { - foreach (AssemblyNameReference assemblyName in assemblies) + HashSet visited = new(); + Queue toVisit = new(); + if (_assembly is not null) + toVisit.Enqueue(_assembly); + while (toVisit.Count > 0) { - if (context.TryResolve(assemblyName) is not AssemblyDefinition assembly) - { - // If we cannot find the assembly, skip it. - // We'll fail at runtime as expected. - continue; - } + var assembly = toVisit.Dequeue(); + visited.Add(assembly); foreach (CustomAttribute attr in assembly.CustomAttributes) { if (attr.AttributeType is not GenericInstanceType @@ -257,9 +276,41 @@ public void Resolve(LinkContext context, TypeMapHandler manager) { manager.AddProxyTypeMapEntry(typeMapGroup, (attr, assembly)); } + else if (attr.AttributeType.Name is "TypeMapAssemblyTargetAttribute`1") + { + manager.AddAssemblyTarget(typeMapGroup, (attr, assembly)); + if (attr.ConstructorArguments[0].Value is string str) + { + var nextAssemblyName = AssemblyNameReference.Parse(str); + if (context.TryResolve(nextAssemblyName) is AssemblyDefinition nextAssembly) + { + if (!visited.Contains(nextAssembly) && !toVisit.Contains(nextAssembly)) + toVisit.Enqueue(nextAssembly); + } + } + } } } } } } -} + + file static class CustomAttributeWithOriginExtensions + { + extension(CustomAttributeWithOrigin self) + { + public bool DependencySourceRequiresTarget(LinkContext context, TypeDefinition sourceType) + { + return self.Attribute.AttributeType.Name switch + { + "TypeMapAttribute`1" => + sourceType is null || context.Annotations.IsRelevantToVariantCasting(sourceType) + || context.Annotations.IsInstantiated(sourceType), + "TypeMapAssociationAttribute`1" => + context.Annotations.IsInstantiated(sourceType), + _ => false, + }; + } + } + } + } diff --git a/src/tools/illink/src/linker/Linker/TypeReferenceEqualityComparer.cs b/src/tools/illink/src/linker/Linker/TypeReferenceEqualityComparer.cs index 6ed69ada047b7c..f7cacb7e047827 100644 --- a/src/tools/illink/src/linker/Linker/TypeReferenceEqualityComparer.cs +++ b/src/tools/illink/src/linker/Linker/TypeReferenceEqualityComparer.cs @@ -11,6 +11,9 @@ namespace Mono.Linker // Copied from https://github.com/jbevain/cecil/blob/master/Mono.Cecil/TypeReferenceComparer.cs internal sealed class TypeReferenceEqualityComparer : EqualityComparer { + [Obsolete("Default will point to default object equality comparer. Use the constructor to create an instance with a resolver.")] + public static new object Default => throw new InvalidOperationException(); + public readonly ITryResolveMetadata _resolver; public TypeReferenceEqualityComparer(ITryResolveMetadata resolver) diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapReferencedAssembly.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapReferencedAssembly.cs new file mode 100644 index 00000000000000..82ea66d3933a6b --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapReferencedAssembly.cs @@ -0,0 +1,51 @@ +// 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.Runtime.InteropServices; +using Mono.Linker.Tests.Cases.Reflection.Dependencies; +using Mono.Linker.Tests.Cases.Reflection.Dependencies.Library; + +[assembly: TypeMap("UnimportantString", typeof(TargetTypeUnconditional1))] +[assembly: TypeMap("UnimportantString", typeof(TargetTypeConditional1), typeof(TrimTarget1))] +[assembly: TypeMapAssociation(typeof(ProxySource1), typeof(ProxyTarget1))] + +[assembly: TypeMap("UnimportantString", typeof(TargetTypeUnconditional2))] +[assembly: TypeMap("UnimportantString", typeof(TargetTypeConditional2), typeof(TrimTarget2))] +[assembly: TypeMapAssociation(typeof(ProxySource2), typeof(ProxyTarget2))] +[assembly: TypeMapAssemblyTarget("library2")] + +namespace Mono.Linker.Tests.Cases.Reflection.Dependencies +{ + public class TypeMapReferencedAssembly + { + public static void Main() + { + // Mark expected trim targets + _ = new TrimTarget1(); + _ = new ProxySource1(); + _ = new TrimTarget2(); + _ = new ProxySource2(); + + // Mark expected type map universe + _ = TypeMapping.GetOrCreateExternalTypeMapping(); + _ = TypeMapping.GetOrCreateProxyTypeMapping(); + } + } + + public class UsedTypeMapUniverse; + public class UnusedTypeMapUniverse; +} + +namespace Mono.Linker.Tests.Cases.Reflection.Dependencies.Library +{ + public class TargetTypeUnconditional1; + public class TargetTypeConditional1; + public class ProxySource1; + public class ProxyTarget1; + public class TargetTypeUnconditional2; + public class TargetTypeConditional2; + public class ProxySource2; + public class ProxyTarget2; + public class TrimTarget1; + public class TrimTarget2; +} diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapSecondOrderReference.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapSecondOrderReference.cs new file mode 100644 index 00000000000000..860d8f2fcc98f4 --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapSecondOrderReference.cs @@ -0,0 +1,43 @@ + +// 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.Runtime.InteropServices; +using Mono.Linker.Tests.Cases.Reflection.Dependencies; +using Mono.Linker.Tests.Cases.Reflection.Dependencies.Library2; + +// Nothing in this assembly should be referenced or rooted directly +// This should validate the if only the type map attributes are kept, the assembly is still preserved +[assembly: TypeMap("UnimportantString", typeof(long))] +[assembly: TypeMap("UnimportantString", typeof(uint), typeof(string))] +[assembly: TypeMapAssociation(typeof(int), typeof(uint))] + +[assembly: TypeMap("UnimportantString", typeof(TargetTypeUnconditional2))] +[assembly: TypeMap("UnimportantString", typeof(TargetTypeConditional2), typeof(TrimTarget2))] +[assembly: TypeMapAssociation(typeof(ProxySource2), typeof(ProxyTarget2))] + +[assembly: TypeMapAssemblyTarget("library")] // Circular reference + +namespace Mono.Linker.Tests.Cases.Reflection.Dependencies +{ + public class TypeMapReferencedAssembly2 + { + } + + public class UsedTypeMapUniverse2; + public class UnusedTypeMapUniverse2; +} + +namespace Mono.Linker.Tests.Cases.Reflection.Dependencies.Library2 +{ + public class TargetTypeUnconditional1; + public class TargetTypeConditional1; + public class ProxySource1; + public class ProxyTarget1; + public class TargetTypeUnconditional2; + public class TargetTypeConditional2; + public class ProxySource2; + public class ProxyTarget2; + public class TrimTarget1; + public class TrimTarget2; +} diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs index 76ce4b0468abad..949a5b5de45ec1 100644 --- a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs @@ -11,9 +11,13 @@ using Mono.Linker.Tests.Cases.Expectations.Assertions; using Mono.Linker.Tests.Cases.Expectations.Metadata; using Mono.Linker.Tests.Cases.Reflection; +using Mono.Linker.Tests.Cases.Reflection.Dependencies; +using Mono.Linker.Tests.Cases.Reflection.Dependencies.Library; -[assembly: KeptAttributeAttribute(typeof(TypeMapAttribute), By = Tool.Trimmer)] -[assembly: KeptAttributeAttribute(typeof(TypeMapAssociationAttribute), By = Tool.Trimmer)] +[assembly: KeptAttributeAttribute(typeof(TypeMapAttribute))] +[assembly: KeptAttributeAttribute(typeof(TypeMapAssociationAttribute))] +[assembly: KeptAttributeAttribute(typeof(TypeMapAssociationAttribute))] +[assembly: KeptAttributeAttribute(typeof(TypeMapAttribute))] [assembly: TypeMap("TrimTargetIsTarget", typeof(TargetAndTrimTarget), typeof(TargetAndTrimTarget))] [assembly: TypeMap("TrimTargetIsUnrelated", typeof(TargetType), typeof(TrimTarget))] [assembly: TypeMap(nameof(AllocatedNoTypeCheckClassTarget), typeof(AllocatedNoTypeCheckClassTarget), typeof(AllocatedNoTypeCheckClass))] @@ -28,6 +32,15 @@ [assembly: TypeMap("Ldobj", typeof(LdobjTarget), typeof(LdobjType))] [assembly: TypeMap("ArrayElement", typeof(ArrayElementTarget), typeof(ArrayElement))] [assembly: TypeMap("TrimTargetIsAllocatedNoTypeCheckNoBoxStruct", typeof(ConstructedNoTypeCheckOrBoxTarget), typeof(ConstructedNoTypeCheckNoBoxStruct))] + +// The TypeMap Universes are kept separate such that Proxy attributes shouldn't be kept if only the External type map is needed. +[assembly: TypeMap("UsedOnlyForExternalTypeMap", typeof(UsedExternalTarget), typeof(UsedTrimTarget))] // Kept +[assembly: TypeMapAssociation(typeof(UsedProxySource), typeof(UsedProxyTarget))] // Removed + +// The TypeMap Universes are kept separate such that External attributes shouldn't be kept if only the Proxy type map is needed. +[assembly: TypeMap("UsedOnlyForExternalTypeMap", typeof(UsedExternalTarget2), typeof(UsedTrimTarget2))] // Removed +[assembly: TypeMapAssociation(typeof(UsedProxySource2), typeof(UsedProxyTarget2))] // Kept + [assembly: TypeMapAssociation(typeof(SourceClass), typeof(ProxyType))] [assembly: TypeMapAssociation(typeof(TypeCheckOnlyClass), typeof(TypeCheckOnlyProxy))] [assembly: TypeMapAssociation(typeof(AllocatedNoBoxStructType), typeof(AllocatedNoBoxProxy))] @@ -40,14 +53,67 @@ [assembly: TypeMap("ClassWithStaticMethod", typeof(TargetType4), typeof(ClassWithStaticMethod))] [assembly: TypeMap("ClassWithStaticMethodAndField", typeof(TargetType5), typeof(ClassWithStaticMethodAndField))] +[assembly: KeptAttributeAttribute(typeof(TypeMapAssemblyTargetAttribute))] +[assembly: TypeMapAssemblyTarget("library")] +// TypeMapAssemblyTarget is kept regardless of which type map the program needs (External or Proxy) +[assembly: KeptAttributeAttribute(typeof(TypeMapAssemblyTargetAttribute))] +[assembly: KeptAttributeAttribute(typeof(TypeMapAssemblyTargetAttribute))] +[assembly: TypeMapAssemblyTarget("library")] +[assembly: TypeMapAssemblyTarget("library")] +[assembly: TypeMapAssemblyTarget("library")] // Should be removed + namespace Mono.Linker.Tests.Cases.Reflection { - [Kept] + [SetupLinkerAction("link", "System.Private.CoreLib")] // Needed to get the RemoveAttributeInstances in embedded xml + [SetupLinkerArgument("--ignore-link-attributes", "false")] [SetupCompileArgument("/unsafe")] + [SetupCompileBefore("library.dll", new[] { "Dependencies/TypeMapReferencedAssembly.cs" })] + [SetupCompileBefore("library2.dll", new[] { "Dependencies/TypeMapSecondOrderReference.cs" })] + [Kept] + [KeptAssembly("library.dll")] + [KeptAssembly("library2.dll")] + [KeptTypeInAssembly("library.dll", typeof(TypeMapReferencedAssembly))] + [KeptMemberInAssembly("library.dll", typeof(TypeMapReferencedAssembly), "Main()")] + [KeptTypeInAssembly("library.dll", typeof(TargetTypeUnconditional1), Tool = Tool.Trimmer)] + [KeptTypeInAssembly("library.dll", typeof(TrimTarget1))] + [KeptMemberInAssembly("library.dll", typeof(TrimTarget1), ".ctor()")] + [KeptTypeInAssembly("library.dll", typeof(TrimTarget2))] + [KeptMemberInAssembly("library.dll", typeof(TrimTarget2), ".ctor()")] + [KeptTypeInAssembly("library.dll", typeof(TargetTypeConditional1), Tool = Tool.Trimmer)] + [KeptTypeInAssembly("library.dll", typeof(ProxySource1))] + [KeptMemberInAssembly("library.dll", typeof(ProxySource1), ".ctor()")] + [KeptTypeInAssembly("library.dll", typeof(ProxyTarget1))] + + [KeptAttributeInAssembly("library.dll", typeof(TypeMapAttribute))] + [KeptAttributeInAssembly("library.dll", typeof(TypeMapAssociationAttribute))] + [KeptAttributeInAssembly("library.dll", typeof(TypeMapAssemblyTargetAttribute))] + [RemovedAttributeInAssembly("library.dll", typeof(TypeMapAttribute))] + [RemovedAttributeInAssembly("library.dll", typeof(TypeMapAssociationAttribute))] + + [KeptAttributeInAssembly("library2.dll", typeof(TypeMapAttribute))] + [KeptAttributeInAssembly("library2.dll", typeof(TypeMapAssociationAttribute))] + [KeptAttributeInAssembly("library2.dll", typeof(TypeMapAssemblyTargetAttribute))] + [RemovedAttributeInAssembly("library2.dll", typeof(TypeMapAttribute))] + [RemovedAttributeInAssembly("library2.dll", typeof(TypeMapAssociationAttribute))] + // No types kept in library2, just TypeMap attributes + [RemovedTypeInAssembly("library2.dll", typeof(TypeMapReferencedAssembly2))] + [RemovedTypeInAssembly("library2.dll", typeof(UsedTypeMapUniverse2))] + [RemovedTypeInAssembly("library2.dll", typeof(UsedTypeMapUniverse2))] + [RemovedTypeInAssembly("library2.dll", typeof(Mono.Linker.Tests.Cases.Reflection.Dependencies.Library2.ProxySource1))] + [RemovedTypeInAssembly("library2.dll", typeof(Mono.Linker.Tests.Cases.Reflection.Dependencies.Library2.ProxySource2))] + [RemovedTypeInAssembly("library2.dll", typeof(Mono.Linker.Tests.Cases.Reflection.Dependencies.Library2.ProxyTarget1))] + [RemovedTypeInAssembly("library2.dll", typeof(Mono.Linker.Tests.Cases.Reflection.Dependencies.Library2.ProxyTarget2))] + [RemovedTypeInAssembly("library2.dll", typeof(Mono.Linker.Tests.Cases.Reflection.Dependencies.Library2.TargetTypeConditional1))] + [RemovedTypeInAssembly("library2.dll", typeof(Mono.Linker.Tests.Cases.Reflection.Dependencies.Library2.TargetTypeConditional2))] + [RemovedTypeInAssembly("library2.dll", typeof(Mono.Linker.Tests.Cases.Reflection.Dependencies.Library2.TargetTypeUnconditional1))] + [RemovedTypeInAssembly("library2.dll", typeof(Mono.Linker.Tests.Cases.Reflection.Dependencies.Library2.TargetTypeUnconditional2))] + [RemovedTypeInAssembly("library2.dll", typeof(Mono.Linker.Tests.Cases.Reflection.Dependencies.Library2.TrimTarget1))] + [RemovedTypeInAssembly("library2.dll", typeof(Mono.Linker.Tests.Cases.Reflection.Dependencies.Library2.TrimTarget2))] class TypeMap { [Kept] [ExpectedWarning("IL2057", "Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String)'")] + [ExpectBodyModified] // Bug public static void Main(string[] args) { object t = Activator.CreateInstance(Type.GetType(args[1])); @@ -114,6 +180,29 @@ static void ConstrainedStaticCall(T t) where T : IStaticInterface Console.WriteLine(new ArrayElement[1]); Console.WriteLine(new ConstructedNoTypeCheckNoBoxStruct(42).Value); + + TypeMapReferencedAssembly.Main(); + + // TypeMapUniverses are independent between External and Proxy type maps. + // That is, if the External type map is used for a given universe, that doesn't keep the Proxy type map, and vice versa. + // Since we only use UsedExternalTypeMap for its External type map, the Proxy type map and its attributes should be removed. + // And vice versa for UsedProxyTypeMap. + _ = TypeMapping.GetOrCreateExternalTypeMapping(); + _ = TypeMapping.GetOrCreateProxyTypeMapping(); + _ = new UsedProxySource(); + _ = new UsedProxySource2(); + object obj = null!; + // Rewritten as if these types are removed + if (obj is UsedTrimTarget) ; + if (obj is UsedTrimTarget2) ; + + // For the second order reference, instantiate int and string as the dependency source types to root the TypeMap attributes + // Do not directly root anything in the assembly to validate the assembly is kept even in only the typemap attributes are marked. + _ = new string('h', 2); + _ = new int(); + _ = TypeMapping.GetOrCreateExternalTypeMapping(); + _ = TypeMapping.GetOrCreateProxyTypeMapping(); + } [Kept] @@ -180,7 +269,7 @@ private static void CheckTrimTarget(object o) [Kept] private static UnboxedOnly Unbox(object o) { - return (UnboxedOnly) o; + return (UnboxedOnly)o; } [Kept] @@ -216,6 +305,7 @@ class ProxyType; [Kept] class UnusedTypeMap; + class UnusedTypeMap2; class UnusedTargetType; class UnusedSourceClass; class UnusedProxyType; @@ -400,57 +490,29 @@ public AllocatedNoBoxStructType(int value) [Kept] class AllocatedNoBoxProxy; -} -// Polyfill for the type map types until we use an LKG runtime that has them with an updated LinkAttributes XML. -namespace System.Runtime.InteropServices -{ - [Kept(By = Tool.Trimmer)] - [KeptBaseType(typeof(Attribute), By = Tool.Trimmer)] - [KeptAttributeAttribute(typeof(AttributeUsageAttribute), By = Tool.Trimmer)] - [KeptAttributeAttribute(typeof(RemoveAttributeInstancesAttribute), By = Tool.Trimmer)] - [RemoveAttributeInstances] - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public sealed class TypeMapAttribute : Attribute - { - [Kept(By = Tool.Trimmer)] - public TypeMapAttribute(string value, Type target) { } + [Kept] + class UsedExternalTypeMap; + [Kept] + class UsedProxyTypeMap; - [Kept(By = Tool.Trimmer)] - [KeptAttributeAttribute(typeof(RequiresUnreferencedCodeAttribute), By = Tool.Trimmer)] - [RequiresUnreferencedCode("Interop types may be removed by trimming")] - public TypeMapAttribute(string value, Type target, Type trimTarget) { } - } + [Kept] + [KeptMember(".ctor()")] + class UsedProxySource; + [Kept] + [KeptMember(".ctor()")] + class UsedProxySource2; + [Kept] + class UsedTrimTarget; + [Kept(By=Tool.NativeAot)] + class UsedTrimTarget2; - [Kept(By = Tool.Trimmer)] - [KeptBaseType(typeof(Attribute), By = Tool.Trimmer)] - [KeptAttributeAttribute(typeof(AttributeUsageAttribute), By = Tool.Trimmer)] - [KeptAttributeAttribute(typeof(RemoveAttributeInstancesAttribute), By = Tool.Trimmer)] - [RemoveAttributeInstances] - [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public sealed class TypeMapAssociationAttribute : Attribute - { - [Kept(By = Tool.Trimmer)] - public TypeMapAssociationAttribute(Type source, Type proxy) { } - } + [Kept] + class UsedExternalTarget; + class UsedExternalTarget2; - [Kept(By = Tool.Trimmer)] - public static class TypeMapping - { - [Kept(By = Tool.Trimmer)] - [KeptAttributeAttribute(typeof(RequiresUnreferencedCodeAttribute), By = Tool.Trimmer)] - [RequiresUnreferencedCode("Interop types may be removed by trimming")] - public static IReadOnlyDictionary GetOrCreateExternalTypeMapping() - { - throw new NotImplementedException($"External type map for {typeof(TTypeMapGroup).Name}"); - } - [Kept(By = Tool.Trimmer)] - [KeptAttributeAttribute(typeof(RequiresUnreferencedCodeAttribute), By = Tool.Trimmer)] - [RequiresUnreferencedCode("Interop types may be removed by trimming")] - public static IReadOnlyDictionary GetOrCreateProxyTypeMapping() - { - throw new NotImplementedException($"Proxy type map for {typeof(TTypeMapGroup).Name}"); - } - } + class UsedProxyTarget; + [Kept] + class UsedProxyTarget2; } From 620e1f7e3daf8ed69ce561a1fbafd0d2a943873e Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 7 Oct 2025 11:37:03 -0700 Subject: [PATCH 2/7] Fix compiler error for last minute change --- src/tools/illink/src/linker/Linker/TypeMapHandler.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs index 708e7c1a4f1499..eb571be3339d03 100644 --- a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs +++ b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs @@ -74,7 +74,8 @@ public void ProcessExternalTypeMapGroupSeen(MethodDefinition callingMethod, Type { foreach (var entry in assemblyTargets) { - _markStep.MarkCustomAttribute(entry.Attribute, new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod), new MessageOrigin(entry.Origin)); + var info = new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod); + _markStep.MarkCustomAttribute(entry.Attribute, info, new MessageOrigin(entry.Origin)); _markStep.MarkAssembly(entry.Origin, info, new MessageOrigin(entry.Origin)); } } @@ -95,7 +96,8 @@ public void ProcessProxyTypeMapGroupSeen(MethodDefinition callingMethod, TypeRef { foreach (var entry in assemblyTargets) { - _markStep.MarkCustomAttribute(entry.Attribute, new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod), new MessageOrigin(entry.Origin)); + var info = new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod); + _markStep.MarkCustomAttribute(entry.Attribute, info, new MessageOrigin(entry.Origin)); _markStep.MarkAssembly(entry.Origin, info, new MessageOrigin(entry.Origin)); } } From 781c1541891fc5419016c492528f65848af62991 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 7 Oct 2025 11:45:45 -0700 Subject: [PATCH 3/7] Update src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs index 949a5b5de45ec1..7e33ec521c8ef1 100644 --- a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs @@ -98,7 +98,6 @@ namespace Mono.Linker.Tests.Cases.Reflection // No types kept in library2, just TypeMap attributes [RemovedTypeInAssembly("library2.dll", typeof(TypeMapReferencedAssembly2))] [RemovedTypeInAssembly("library2.dll", typeof(UsedTypeMapUniverse2))] - [RemovedTypeInAssembly("library2.dll", typeof(UsedTypeMapUniverse2))] [RemovedTypeInAssembly("library2.dll", typeof(Mono.Linker.Tests.Cases.Reflection.Dependencies.Library2.ProxySource1))] [RemovedTypeInAssembly("library2.dll", typeof(Mono.Linker.Tests.Cases.Reflection.Dependencies.Library2.ProxySource2))] [RemovedTypeInAssembly("library2.dll", typeof(Mono.Linker.Tests.Cases.Reflection.Dependencies.Library2.ProxyTarget1))] From af79f1ff3e4530860d9ed53060f3153dbce8f753 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Thu, 9 Oct 2025 11:41:23 -0700 Subject: [PATCH 4/7] PR Feedback --- .../src/linker/Linker/DependencyInfo.cs | 1 + .../src/linker/Linker/TypeMapHandler.cs | 58 ++++++++++++------- .../Reflection/TypeMap.cs | 20 +++++-- 3 files changed, 53 insertions(+), 26 deletions(-) diff --git a/src/tools/illink/src/linker/Linker/DependencyInfo.cs b/src/tools/illink/src/linker/Linker/DependencyInfo.cs index 0f3e02ee145819..7a72ee84b7cc70 100644 --- a/src/tools/illink/src/linker/Linker/DependencyInfo.cs +++ b/src/tools/illink/src/linker/Linker/DependencyInfo.cs @@ -147,6 +147,7 @@ public enum DependencyKind UnsafeAccessorTarget = 89, // the member is referenced via UnsafeAccessor attribute TypeMapEntry = 90, // external or proxy type map entry + TypeMapAssemblyTarget = 91, // TypeMapTargetAssembly for a used typeMapGroup } public readonly struct DependencyInfo : IEquatable diff --git a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs index eb571be3339d03..67035f884f19b1 100644 --- a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs +++ b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs @@ -74,9 +74,8 @@ public void ProcessExternalTypeMapGroupSeen(MethodDefinition callingMethod, Type { foreach (var entry in assemblyTargets) { - var info = new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod); - _markStep.MarkCustomAttribute(entry.Attribute, info, new MessageOrigin(entry.Origin)); - _markStep.MarkAssembly(entry.Origin, info, new MessageOrigin(entry.Origin)); + var info = new DependencyInfo(DependencyKind.TypeMapAssemblyTarget, callingMethod); + MarkTypeMapAttribute(entry, info); } } } @@ -96,9 +95,8 @@ public void ProcessProxyTypeMapGroupSeen(MethodDefinition callingMethod, TypeRef { foreach (var entry in assemblyTargets) { - var info = new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod); - _markStep.MarkCustomAttribute(entry.Attribute, info, new MessageOrigin(entry.Origin)); - _markStep.MarkAssembly(entry.Origin, info, new MessageOrigin(entry.Origin)); + var info = new DependencyInfo(DependencyKind.TypeMapAssemblyTarget, callingMethod); + MarkTypeMapAttribute(entry, info); } } } @@ -109,8 +107,7 @@ void MarkTypeMapAttribute(CustomAttributeWithOrigin entry, DependencyInfo info) _markStep.MarkAssembly(entry.Origin, info, new MessageOrigin(entry.Origin)); // Mark the target type as instantiated - TypeReference targetType = (TypeReference)entry.Attribute.ConstructorArguments[1].Value; - if (targetType is not null && _context.Resolve(targetType) is TypeDefinition targetTypeDef) + if (entry.TargetType is { } targetType && _context.Resolve(targetType) is TypeDefinition targetTypeDef) _context.Annotations.MarkInstantiated(targetTypeDef); } @@ -200,7 +197,7 @@ private void AddAssemblyTarget(TypeReference typeMapGroup, CustomAttributeWithOr } - void RecordTypeMapEntry(CustomAttributeWithOrigin attr, TypeReference group, TypeReference? dependencySource, Dictionary>> unmarkedEntryList, HashSet seenTypeGroups, Dictionary> pendingEntryList) + void RecordTypeMapEntry(CustomAttributeWithOrigin attr, TypeReference group, TypeReference? dependencySource, Dictionary>> pendingDependencySourceMarking, HashSet seenTypeGroups, Dictionary> pendingTypeMapGroupMarking) { if (dependencySource is null) { @@ -213,7 +210,7 @@ void RecordTypeMapEntry(CustomAttributeWithOrigin attr, TypeReference group, Typ } else { - pendingEntryList.AddToList(group, attr); + pendingTypeMapGroupMarking.AddToList(group, attr); return; } } @@ -224,18 +221,25 @@ void RecordTypeMapEntry(CustomAttributeWithOrigin attr, TypeReference group, Typ return; // Couldn't find the type we were asked about. } - if (seenTypeGroups.Contains(group) && attr.DependencySourceRequiresTarget(_context, dependencyTypeDef)) + if (attr.DependencySourceRequiresTarget(_context, dependencyTypeDef)) { - MarkTypeMapAttribute(attr, new DependencyInfo(DependencyKind.TypeMapEntry, dependencySource)); + if (seenTypeGroups.Contains(group)) + { + MarkTypeMapAttribute(attr, new DependencyInfo(DependencyKind.TypeMapEntry, dependencySource)); + } + else + { + pendingTypeMapGroupMarking.AddToList(group, attr); + } } else { - if (!unmarkedEntryList.TryGetValue(dependencyTypeDef, out Dictionary>? entries)) + if (!pendingDependencySourceMarking.TryGetValue(dependencyTypeDef, out Dictionary>? entries)) { entries = new(new TypeReferenceEqualityComparer(_context)) { { group, [] } }; - unmarkedEntryList[dependencyTypeDef] = entries; + pendingDependencySourceMarking[dependencyTypeDef] = entries; } entries.AddToList(group, attr); @@ -251,14 +255,14 @@ class TypeMapResolver(AssemblyDefinition? _assembly) { public void Resolve(LinkContext context, TypeMapHandler manager) { - HashSet visited = new(); + if (_assembly is null) + return; + HashSet seen = new(); Queue toVisit = new(); - if (_assembly is not null) - toVisit.Enqueue(_assembly); + toVisit.Enqueue(_assembly); while (toVisit.Count > 0) { var assembly = toVisit.Dequeue(); - visited.Add(assembly); foreach (CustomAttribute attr in assembly.CustomAttributes) { if (attr.AttributeType is not GenericInstanceType @@ -286,8 +290,13 @@ public void Resolve(LinkContext context, TypeMapHandler manager) var nextAssemblyName = AssemblyNameReference.Parse(str); if (context.TryResolve(nextAssemblyName) is AssemblyDefinition nextAssembly) { - if (!visited.Contains(nextAssembly) && !toVisit.Contains(nextAssembly)) +#pragma warning disable CA1868 // Unnecessary call to 'Contains(item)' + if (!seen.Contains(nextAssembly)) + { + seen.Add(nextAssembly); toVisit.Enqueue(nextAssembly); + } +#pragma warning restore CA1868 // Unnecessary call to 'Contains(item)' } } } @@ -307,12 +316,21 @@ public bool DependencySourceRequiresTarget(LinkContext context, TypeDefinition s { "TypeMapAttribute`1" => sourceType is null || context.Annotations.IsRelevantToVariantCasting(sourceType) - || context.Annotations.IsInstantiated(sourceType), + || context.Annotations.IsInstantiated(sourceType), // Or target of an IsInst instruction "TypeMapAssociationAttribute`1" => context.Annotations.IsInstantiated(sourceType), _ => false, }; } + + public TypeReference? TargetType => + self.Attribute.AttributeType.Name switch + { + "TypeMapAttribute`1" or "TypeMapAssociationAttribute`1" => + (TypeReference)self.Attribute.ConstructorArguments[1].Value!, + _ => null + }; + } } } diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs index 949a5b5de45ec1..9d12d3f374c3b2 100644 --- a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs @@ -113,7 +113,6 @@ class TypeMap { [Kept] [ExpectedWarning("IL2057", "Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String)'")] - [ExpectBodyModified] // Bug public static void Main(string[] args) { object t = Activator.CreateInstance(Type.GetType(args[1])); @@ -191,10 +190,8 @@ static void ConstrainedStaticCall(T t) where T : IStaticInterface _ = TypeMapping.GetOrCreateProxyTypeMapping(); _ = new UsedProxySource(); _ = new UsedProxySource2(); - object obj = null!; - // Rewritten as if these types are removed - if (obj is UsedTrimTarget) ; - if (obj is UsedTrimTarget2) ; + + UseTrimTargets(); // For the second order reference, instantiate int and string as the dependency source types to root the TypeMap attributes // Do not directly root anything in the assembly to validate the assembly is kept even in only the typemap attributes are marked. @@ -202,7 +199,18 @@ static void ConstrainedStaticCall(T t) where T : IStaticInterface _ = new int(); _ = TypeMapping.GetOrCreateExternalTypeMapping(); _ = TypeMapping.GetOrCreateProxyTypeMapping(); + } + [ExpectBodyModified] + [Kept] + static void UseTrimTargets() + { + object obj = null!; + // Rewritten as if these types are removed + if (obj is UsedTrimTarget) + obj = 1; + if (obj is UsedTrimTarget2) + obj = 2; } [Kept] @@ -504,7 +512,7 @@ class UsedProxySource; class UsedProxySource2; [Kept] class UsedTrimTarget; - [Kept(By=Tool.NativeAot)] + [Kept(By=Tool.NativeAot)] // Associated attribute not kept class UsedTrimTarget2; [Kept] From 551d9755767ba7d67b25a83bc96728a8fb5a0b63 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 13 Oct 2025 11:46:40 -0700 Subject: [PATCH 5/7] Check return value of Add rather than use Contains() --- src/tools/illink/src/linker/Linker/TypeMapHandler.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs index 67035f884f19b1..8ea6cb5cdf5e02 100644 --- a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs +++ b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs @@ -290,13 +290,10 @@ public void Resolve(LinkContext context, TypeMapHandler manager) var nextAssemblyName = AssemblyNameReference.Parse(str); if (context.TryResolve(nextAssemblyName) is AssemblyDefinition nextAssembly) { -#pragma warning disable CA1868 // Unnecessary call to 'Contains(item)' - if (!seen.Contains(nextAssembly)) + if (seen.Add(nextAssembly)) { - seen.Add(nextAssembly); toVisit.Enqueue(nextAssembly); } -#pragma warning restore CA1868 // Unnecessary call to 'Contains(item)' } } } From edaa68992d1a560321f18ad0b1c5cac9accdc309 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Mon, 13 Oct 2025 12:24:58 -0700 Subject: [PATCH 6/7] Leave marking unchanged for follow-up --- src/tools/illink/src/linker/Linker/TypeMapHandler.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs index 8ea6cb5cdf5e02..48d30bcfa580ce 100644 --- a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs +++ b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs @@ -312,8 +312,7 @@ public bool DependencySourceRequiresTarget(LinkContext context, TypeDefinition s return self.Attribute.AttributeType.Name switch { "TypeMapAttribute`1" => - sourceType is null || context.Annotations.IsRelevantToVariantCasting(sourceType) - || context.Annotations.IsInstantiated(sourceType), // Or target of an IsInst instruction + sourceType is null || context.Annotations.IsMarked(sourceType), "TypeMapAssociationAttribute`1" => context.Annotations.IsInstantiated(sourceType), _ => false, From fd8be1059b66153a7235836107a1c10f9fd0cf31 Mon Sep 17 00:00:00 2001 From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com> Date: Tue, 14 Oct 2025 10:30:41 -0700 Subject: [PATCH 7/7] Revert marking change to match old behavior. --- src/tools/illink/src/linker/Linker/TypeMapHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs index 48d30bcfa580ce..5cadd46b65851b 100644 --- a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs +++ b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs @@ -314,7 +314,7 @@ public bool DependencySourceRequiresTarget(LinkContext context, TypeDefinition s "TypeMapAttribute`1" => sourceType is null || context.Annotations.IsMarked(sourceType), "TypeMapAssociationAttribute`1" => - context.Annotations.IsInstantiated(sourceType), + context.Annotations.IsMarked(sourceType), _ => false, }; }