Skip to content

Conversation

jtschuster
Copy link
Member

Fixes #120394

  • Mark TypeMapAssemblyTargetAttributes when its TypeMapUniverse is marked
  • Recurse through TypeMapAssemblyTarget assemblies to find all TypeMapAttributes
  • Refactor TypeMapHandler construction and initialization
  • Use TypeReferenceEqualityComparer for Dictionaries
  • Mark TypeMap attribute origin / assembly when a TypeMap attribute is marked. Otherwise, assemblies with only TypeMapAttributes will not be kept.
  • Add more test coverage

- 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
@Copilot Copilot AI review requested due to automatic review settings October 7, 2025 18:31
@github-actions github-actions bot added the area-Tools-ILLink .NET linker development as well as trimming analyzers label Oct 7, 2025
@dotnet-policy-service dotnet-policy-service bot added the linkable-framework Issues associated with delivering a linker friendly framework label Oct 7, 2025
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR updates TypeMap attribute handling in the IL linker to fix issues with assembly preservation and attribute marking. The changes improve how TypeMapAttributes are handled across assemblies and ensure assemblies containing only TypeMap attributes are properly preserved.

Key changes:

  • Enhanced TypeMapHandler to recursively process TypeMapAssemblyTarget assemblies
  • Improved attribute marking logic to mark assemblies when TypeMap attributes need to be preserved
  • Added comprehensive test coverage for cross-assembly TypeMap scenarios

Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs Extended test case with cross-assembly TypeMap scenarios and dependency validation
src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapSecondOrderReference.cs New test dependency assembly with TypeMap attributes for second-order reference testing
src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapReferencedAssembly.cs New test dependency assembly with TypeMap attributes for primary reference testing
src/tools/illink/src/linker/Linker/TypeReferenceEqualityComparer.cs Added obsolete marker for Default property to prevent misuse
src/tools/illink/src/linker/Linker/TypeMapHandler.cs Major refactor of TypeMapHandler with improved initialization, recursive assembly processing, and proper assembly marking
src/tools/illink/src/linker/Linker.Steps/SweepStep.cs Updated IsMarkedAssembly to check both assembly and main module marking
src/tools/illink/src/linker/Linker.Steps/MarkStep.cs Simplified TypeMapHandler initialization and made MarkAssembly method public

Copy link
Contributor

Tagging subscribers to this area: @dotnet/illink
See info in area-owners.md if you want to be subscribed.

@Sergio0694
Copy link
Contributor

@jkoritzinsky does this need to be backported to .NET 10? As in, I assume this bug would break CsWinRT 3.0 too? 🤔

bool IsMarkedAssembly(AssemblyDefinition assembly)
{
return Annotations.IsMarked(assembly.MainModule);
return Annotations.IsMarked(assembly) || Annotations.IsMarked(assembly.MainModule);
Copy link
Member Author

Choose a reason for hiding this comment

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

An assembly that only had MarkAssembly called would be not be kept if the MainModule wasn't marked. If it makes more sense to mark the MainModule in MarkAssembly rather than have this check, I'm happy to change.

internal sealed class TypeReferenceEqualityComparer : EqualityComparer<TypeReference>
{
[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();
Copy link
Member Author

Choose a reason for hiding this comment

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

Accidentally used TypeReferenceEqualityComparer.Default, which went to ObjectEqualityComparer.Default and required more debugging time than it should have. Adding this property to avoid doing that again.

@AaronRobinsonMSFT AaronRobinsonMSFT added this to the 11.0.0 milestone Oct 8, 2025
public void Resolve(LinkContext context, TypeMapHandler manager)
{
foreach (AssemblyNameReference assemblyName in assemblies)
HashSet<AssemblyDefinition> visited = new();
Copy link
Member

Choose a reason for hiding this comment

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

nit: check _assembly here and return before allocating anything if it's null

var nextAssemblyName = AssemblyNameReference.Parse(str);
if (context.TryResolve(nextAssemblyName) is AssemblyDefinition nextAssembly)
{
if (!visited.Contains(nextAssembly) && !toVisit.Contains(nextAssembly))
Copy link
Member

Choose a reason for hiding this comment

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

Add items to visited on enqueue above, instead of dequeue, then we can avoid the linear scan through toVisit here.

MarkTypeMapAttribute(entry, new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod));
foreach (var entry in assemblyTargets)
{
var info = new DependencyInfo(DependencyKind.TypeMapEntry, callingMethod);
Copy link
Member

Choose a reason for hiding this comment

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

nit: TypeMapEntry doesn't seem like it quite fits the usage - maybe add another DependencyKind?

{
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));
Copy link
Member

Choose a reason for hiding this comment

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

Can this use MarkTypeMapAttribute instead? Same below.

if (seenTypeGroups.Contains(group) && attr.DependencySourceRequiresTarget(_context, dependencyTypeDef))
{
MarkTypeMapAttribute(attr, new DependencyInfo(DependencyKind.TypeMapEntry, trimTarget));
MarkTypeMapAttribute(attr, new DependencyInfo(DependencyKind.TypeMapEntry, dependencySource));
Copy link
Member

Choose a reason for hiding this comment

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

This will go to the else case even if the dependency source doesn't require the target - should the seenTypeGroups and the else below be inside the DependencySourceRequiresTarget check?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, good catch. We should split first on DependencySourceRequiresTarget(), then seenTypeGroups.Contains() to match the other processing flow (pending dependency source -> pending typeMapGroup -> mark).

{
"TypeMapAttribute`1" =>
sourceType is null || context.Annotations.IsRelevantToVariantCasting(sourceType)
|| context.Annotations.IsInstantiated(sourceType),
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 for the external type map, the types aren't required to be instantiated or marked relevant to variant casting. I think the _typeMapHandler.ProcessType(type) in MarkInstruction is intended to keep type map entries in more cases.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I meant to leave a comment that it also should require the target when the source is a target of an IsInst instruction. "Target of IsInst" just doesn't have an associated annotation and I'm a little hesitant to add another cache for this. This should handle all "annotatable" ways that the target type is required at initialization. I think as long as this is only called during initialization and we keep the ProcessType() call for IsInst instructions we should be okay. I can add a comment to make that clear or refactor it to make it only visible to RecordTypeMapEntry.

{
[Kept]
[ExpectedWarning("IL2057", "Unrecognized value passed to the parameter 'typeName' of method 'System.Type.GetType(String)'")]
[ExpectBodyModified] // Bug
Copy link
Member

Choose a reason for hiding this comment

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

What's the bug?

Copy link
Member Author

Choose a reason for hiding this comment

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

Thanks for noting this, I forgot to add a comment. It looks like the UnusedTypeChecks optimization is messing with the TypeMap attributes. The if (obj is UsedTrimTarget) checks are being modified. I'll a better example and a github issue for this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-Tools-ILLink .NET linker development as well as trimming analyzers linkable-framework Issues associated with delivering a linker friendly framework
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

Trimmer doesn't keep TypeMapAssemblyTargetAttributes
4 participants