File: Linker.Dataflow\DynamicallyAccessedMembersTypeHierarchy.cs
Web Access
Project: src\src\tools\illink\src\linker\Mono.Linker.csproj (illink)
// 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.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using ILLink.Shared;
using Mono.Cecil;
using Mono.Linker.Steps;
 
namespace Mono.Linker.Dataflow
{
    sealed class DynamicallyAccessedMembersTypeHierarchy
    {
        readonly LinkContext _context;
        readonly MarkStep _markStep;
        readonly ReflectionMarker _reflectionMarker;
 
        // Cache of DynamicallyAccessedMembers annotations applied to types and their hierarchies
        // Values
        //   annotation - the aggregated annotation value from the entire base and interface hierarchy of the given type
        //                If the type has a base class with annotation a1 and an interface with annotation a2, the stored
        //                annotation is a1 | a2.
        //   applied - set to true once the annotation was applied to the type
        //             This only happens once the right reflection pattern is found.
        //             If a new type is being marked and one of its base types/interface has the applied set to true
        //             the new type will apply its annotation and will also set its applied to true.
        // Non-interface types
        //   - Only marked types with non-empty annotation are put into the cache
        //   - Non-marked types are not stored in the cache
        //   - Marked types which are not in the cache don't have any annotation
        // Interface types
        //   - All interface types accessible from marked types are stored in the cache
        //   - If the interface type doesn't have annotation the value None is stored here
        //
        // It's not possible to use the marking as a filter for interfaces in the cache
        // because interfaces are marked late and in effectively random order.
        // For this cache to be effective we need to be able to fill it for all base types and interfaces
        // of a type which is currently being marked - at which point the interfaces are not yet marked.
        readonly Dictionary<TypeDefinition, (DynamicallyAccessedMemberTypes annotation, bool applied)> _typesInDynamicallyAccessedMembersHierarchy;
 
        public DynamicallyAccessedMembersTypeHierarchy(LinkContext context, MarkStep markStep)
        {
            _context = context;
            _markStep = markStep;
            _typesInDynamicallyAccessedMembersHierarchy = new Dictionary<TypeDefinition, (DynamicallyAccessedMemberTypes, bool)>();
            _reflectionMarker = new ReflectionMarker(_context, _markStep, enabled: true);
        }
 
        public (DynamicallyAccessedMemberTypes annotation, bool applied) ProcessMarkedTypeForDynamicallyAccessedMembersHierarchy(TypeDefinition type)
        {
            // We'll use the cache also as a way to detect and avoid recursion for interfaces and annotated base types
            if (_typesInDynamicallyAccessedMembersHierarchy.TryGetValue(type, out var existingValue))
                return existingValue;
 
            DynamicallyAccessedMemberTypes annotation = _context.Annotations.FlowAnnotations.GetTypeAnnotation(type);
            bool apply = false;
 
            if (type.IsInterface)
                _typesInDynamicallyAccessedMembersHierarchy.Add(type, (annotation, false));
 
            TypeDefinition? baseType = _context.TryResolve(type.BaseType);
            if (baseType != null)
            {
                var baseValue = ProcessMarkedTypeForDynamicallyAccessedMembersHierarchy(baseType);
                annotation |= baseValue.annotation;
                apply |= baseValue.applied;
            }
 
            // For the purposes of the DynamicallyAccessedMembers type hierarchies
            // we consider interfaces of marked types to be also "marked" in that
            // their annotations will be applied to the type regardless if later on
            // we decide to remove the interface. This is to keep the complexity of the implementation
            // relatively low. In the future it could be possibly optimized.
            if (type.HasInterfaces)
            {
                foreach (InterfaceImplementation iface in type.Interfaces)
                {
                    var interfaceType = _context.TryResolve(iface.InterfaceType);
                    if (interfaceType != null)
                    {
                        var interfaceValue = ProcessMarkedTypeForDynamicallyAccessedMembersHierarchy(interfaceType);
                        annotation |= interfaceValue.annotation;
                        apply |= interfaceValue.applied;
                    }
                }
            }
 
            Debug.Assert(!apply || annotation != DynamicallyAccessedMemberTypes.None);
 
            // If OptimizeTypeHierarchyAnnotations is disabled, we will apply the annotations without seeing object.GetType()
            bool applyOptimizeTypeHierarchyAnnotations = (annotation != DynamicallyAccessedMemberTypes.None) && !_context.IsOptimizationEnabled(CodeOptimizations.OptimizeTypeHierarchyAnnotations, type);
            // Unfortunately, we cannot apply the annotation to type derived from EventSource - Revisit after https://github.com/dotnet/runtime/issues/54859
            // Breaking the logic to make it easier to maintain in the future since the logic is convoluted
            // DisableEventSourceSpecialHandling is closely tied to a type derived from EventSource and should always go together
            // However, logically it should be possible to use DisableEventSourceSpecialHandling to allow marking types derived from EventSource when OptimizeTypeHierarchyAnnotations is disabled
            apply |= applyOptimizeTypeHierarchyAnnotations && (_context.DisableEventSourceSpecialHandling || !BCL.EventTracingForWindows.IsEventSourceImplementation(type, _context));
 
            // Store the results in the cache
            // Don't store empty annotations for non-interface types - we can use the presence of the row
            // in the cache as indication of it instead.
            // This doesn't work for interfaces, since we can't rely on them being marked (and thus have the cache
            // already filled), so we need to always store the row (even if empty) for interfaces.
            if (annotation != DynamicallyAccessedMemberTypes.None || type.IsInterface)
            {
                _typesInDynamicallyAccessedMembersHierarchy[type] = (annotation, apply);
            }
 
            // It's important to first store the annotation in the cache and only then apply the annotation.
            // Applying the annotation will lead to marking additional types which in turn calls back into this
            // method to look for annotations. If the newly marked type derives from the one we're processing
            // it will rely on the cache to know if it's annotated - so the record must be in the cache
            // before it happens.
            if (apply)
            {
                // One of the base/interface types is already marked as having the annotation applied
                // so we need to apply the annotation to this type as well
                // Report warnings on access to annotated members, with the annotated type as the origin.
                ApplyDynamicallyAccessedMembersToType(type, annotation);
            }
 
            return (annotation, apply);
        }
 
        public DynamicallyAccessedMemberTypes ApplyDynamicallyAccessedMembersToTypeHierarchy(TypeDefinition type)
        {
            (var annotation, var applied) = ProcessMarkedTypeForDynamicallyAccessedMembersHierarchy(type);
 
            // If the annotation was already applied to this type, there's no reason to repeat the operation, the result will
            // be no change.
            if (applied || annotation == DynamicallyAccessedMemberTypes.None)
                return annotation;
 
            // Apply the effective annotation for the type
            // Report warnings on access to annotated members, with the annotated type as the origin.
            ApplyDynamicallyAccessedMembersToType(type, annotation);
 
            // Mark it as applied in the cache
            _typesInDynamicallyAccessedMembersHierarchy[type] = (annotation, true);
 
            // Propagate the newly applied annotation to all derived/implementation types
            // Since we don't have a data structure which would allow us to enumerate all derived/implementation types
            // walk all of the types in the cache. These are good candidates as types not in the cache don't apply.
            //
            // Applying annotations can lead to marking additional types which can lead to adding new records
            // to the cache. So we can't simply iterate over the cache. We also can't rely on the auto-applying annotations
            // which is triggered from marking via ProcessMarkedTypeForDynamicallyAccessedMembersHierarchy as that will
            // only reliably work once the annotations are applied to all types in the cache first. Partially
            // applied annotations to the cache are not enough. So we have to apply the annotations to any types
            // added to the cache during the application as well.
            //
            HashSet<TypeDefinition> typesProcessed = new HashSet<TypeDefinition>();
            List<TypeDefinition> candidateTypes = new List<TypeDefinition>();
            while (true)
            {
                candidateTypes.Clear();
                foreach (var candidate in _typesInDynamicallyAccessedMembersHierarchy)
                {
                    if (candidate.Value.annotation == DynamicallyAccessedMemberTypes.None || candidate.Value.applied)
                        continue;
 
                    if (typesProcessed.Add(candidate.Key))
                        candidateTypes.Add(candidate.Key);
                }
 
                if (candidateTypes.Count == 0)
                    break;
 
                foreach (var candidateType in candidateTypes)
                {
                    ApplyDynamicallyAccessedMembersToTypeHierarchyInner(candidateType);
                }
            }
 
            return annotation;
        }
 
        bool ApplyDynamicallyAccessedMembersToTypeHierarchyInner(TypeDefinition type)
        {
            (var annotation, var applied) = GetCachedInfoForTypeInHierarchy(type);
 
            if (annotation == DynamicallyAccessedMemberTypes.None)
                return false;
 
            if (applied)
                return true;
 
            TypeDefinition? baseType = _context.TryResolve(type.BaseType);
            if (baseType != null)
                applied = ApplyDynamicallyAccessedMembersToTypeHierarchyInner(baseType);
 
            if (!applied && type.HasInterfaces)
            {
                foreach (InterfaceImplementation iface in type.Interfaces)
                {
                    var interfaceType = _context.TryResolve(iface.InterfaceType);
                    if (interfaceType != null)
                    {
                        if (ApplyDynamicallyAccessedMembersToTypeHierarchyInner(interfaceType))
                        {
                            applied = true;
                            break;
                        }
                    }
                }
            }
 
            if (applied)
            {
                // Report warnings on access to annotated members, with the annotated type as the origin.
                ApplyDynamicallyAccessedMembersToType(type, annotation);
                _typesInDynamicallyAccessedMembersHierarchy[type] = (annotation, true);
            }
 
            return applied;
        }
 
        void ApplyDynamicallyAccessedMembersToType(TypeDefinition type, DynamicallyAccessedMemberTypes annotation)
        {
            var origin = new MessageOrigin(type);
            Debug.Assert(annotation != DynamicallyAccessedMemberTypes.None);
 
            // We need to apply annotations to this type, and its base/interface types (recursively)
            // But the annotations on base will be applied so we don't need to apply those
            // again (and should avoid doing so as it would produce extra warnings).
            var baseType = _context.TryResolve(type.BaseType);
            if (baseType != null)
            {
                var baseAnnotation = GetCachedInfoForTypeInHierarchy(baseType);
                if (!baseAnnotation.applied && baseAnnotation.annotation != DynamicallyAccessedMemberTypes.None)
                    ApplyDynamicallyAccessedMembersToType(baseType, baseAnnotation.annotation);
                var annotationToApplyToBase = Annotations.GetMissingMemberTypes(annotation, baseAnnotation.annotation);
 
                // Apply any annotations that didn't exist on the base type to the base type.
                // This may produce redundant warnings when the annotation is DAMT.All or DAMT.PublicConstructors and the base already has a
                // subset of those annotations.
                _reflectionMarker.MarkTypeForDynamicallyAccessedMembers(origin, baseType, annotationToApplyToBase, DependencyKind.DynamicallyAccessedMemberOnType, declaredOnly: false);
            }
 
            // Most of the DynamicallyAccessedMemberTypes don't select members on interfaces. We only need to apply
            // annotations to interfaces separately if dealing with DAMT.All or DAMT.Interfaces.
            if (annotation.HasFlag(DynamicallyAccessedMemberTypes.Interfaces) && type.HasInterfaces)
            {
                var annotationToApplyToInterfaces = annotation == DynamicallyAccessedMemberTypes.All ? annotation : DynamicallyAccessedMemberTypes.Interfaces;
                foreach (var iface in type.Interfaces)
                {
                    var interfaceType = _context.TryResolve(iface.InterfaceType);
                    if (interfaceType == null)
                        continue;
 
                    var interfaceAnnotation = GetCachedInfoForTypeInHierarchy(interfaceType);
                    if (interfaceAnnotation.annotation.HasFlag(annotationToApplyToInterfaces))
                    {
                        if (!interfaceAnnotation.applied)
                            ApplyDynamicallyAccessedMembersToType(interfaceType, interfaceAnnotation.annotation);
                    }
                    else
                    {
                        // Apply All or Interfaces to the interface type.
                        // DAMT.All may produce redundant warnings from implementing types, when the interface type already had some annotations.
                        _reflectionMarker.MarkTypeForDynamicallyAccessedMembers(origin, interfaceType, annotationToApplyToInterfaces, DependencyKind.DynamicallyAccessedMemberOnType, declaredOnly: false);
                    }
                }
            }
 
            // The annotations this type inherited from its base types or interfaces should not produce
            // warnings on the respective base/interface members, since those are already covered by applying
            // the annotations to those types. So we only need to handle the members directly declared on this type.
            _reflectionMarker.MarkTypeForDynamicallyAccessedMembers(origin, type, annotation, DependencyKind.DynamicallyAccessedMemberOnType, declaredOnly: true);
        }
 
        (DynamicallyAccessedMemberTypes annotation, bool applied) GetCachedInfoForTypeInHierarchy(TypeDefinition type)
        {
            // The type should be in our cache already
            if (!_typesInDynamicallyAccessedMembersHierarchy.TryGetValue(type, out var existingValue))
            {
                // If it's not in the cache it should be a non-interface type in which case it means there were no annotations
                Debug.Assert(!type.IsInterface);
                return (DynamicallyAccessedMemberTypes.None, false);
            }
 
            return existingValue;
        }
    }
}