File: src\ExpressionEvaluator\Core\Source\ResultProvider\Expansion\MemberExpansion.cs
Web Access
Project: src\src\ExpressionEvaluator\Core\Source\ResultProvider\Portable\Microsoft.CodeAnalysis.ResultProvider.csproj (Microsoft.CodeAnalysis.ExpressionEvaluator.ResultProvider)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using Microsoft.VisualStudio.Debugger.Clr;
using Microsoft.VisualStudio.Debugger.ComponentInterfaces;
using Microsoft.VisualStudio.Debugger.Evaluation;
using Microsoft.VisualStudio.Debugger.Evaluation.ClrCompilation;
using Microsoft.VisualStudio.Debugger.Metadata;
using Type = Microsoft.VisualStudio.Debugger.Metadata.Type;
 
namespace Microsoft.CodeAnalysis.ExpressionEvaluator
{
    /// <summary>
    /// Type member expansion.
    /// </summary>
    /// <remarks>
    /// Includes accesses to static members with instance receivers and
    /// accesses to instance members with dynamic receivers.
    /// </remarks>
    internal sealed class MemberExpansion : Expansion
    {
        internal static Expansion CreateExpansion(
            DkmInspectionContext inspectionContext,
            TypeAndCustomInfo declaredTypeAndInfo,
            DkmClrValue value,
            ExpansionFlags flags,
            Predicate<MemberInfo> predicate,
            ResultProvider resultProvider,
            bool isProxyType,
            bool supportsFavorites)
        {
            // For members of type DynamicProperty (part of Dynamic View expansion), we want
            // to expand the underlying value (not the members of the DynamicProperty type).
            var type = value.Type;
            var runtimeType = type.GetLmrType();
            var isDynamicProperty = runtimeType.IsDynamicProperty();
            if (isDynamicProperty)
            {
                Debug.Assert(!value.IsNull);
                value = value.GetFieldValue("value", inspectionContext);
            }
 
            // Primitives, enums, function pointers, IntPtr, UIntPtr and null values with a declared type that is an interface have no visible members.
            Debug.Assert(!runtimeType.IsInterface || value.IsNull);
            if (resultProvider.IsPrimitiveType(runtimeType) || runtimeType.IsEnum || runtimeType.IsInterface || runtimeType.IsFunctionPointer() ||
                runtimeType.IsIntPtr() || runtimeType.IsUIntPtr())
            {
                return null;
            }
 
            // As in the old C# EE, DynamicProperty members are only expandable if they have a Dynamic View expansion.
            var dynamicViewExpansion = DynamicViewExpansion.CreateExpansion(inspectionContext, value);
            if (isDynamicProperty && (dynamicViewExpansion == null))
            {
                return null;
            }
 
            var customTypeInfoMap = CustomTypeInfoTypeArgumentMap.Create(declaredTypeAndInfo);
 
            var expansions = ArrayBuilder<Expansion>.GetInstance();
 
            // Expand members. TODO: Ideally, this would be done lazily (https://github.com/dotnet/roslyn/issues/32800)
            // From the members, collect the fields and properties,
            // separated into static and instance members.
            var staticMembers = ArrayBuilder<MemberAndDeclarationInfo>.GetInstance();
            var instanceMembers = ArrayBuilder<MemberAndDeclarationInfo>.GetInstance();
            var appDomain = value.Type.AppDomain;
 
            var allMembers = ArrayBuilder<MemberAndDeclarationInfo>.GetInstance();
            var includeInherited = (flags & ExpansionFlags.IncludeBaseMembers) == ExpansionFlags.IncludeBaseMembers;
            var hideNonPublic = (inspectionContext.EvaluationFlags & DkmEvaluationFlags.HideNonPublicMembers) == DkmEvaluationFlags.HideNonPublicMembers;
            var raw = (inspectionContext.EvaluationFlags & DkmEvaluationFlags.ShowValueRaw) == DkmEvaluationFlags.ShowValueRaw;
            var favoritesInfo = supportsFavorites ? type.GetFavorites() : null;
            runtimeType.AppendTypeMembers(allMembers, predicate, resultProvider, declaredTypeAndInfo.Type, appDomain, includeInherited, hideNonPublic, isProxyType, raw, supportsFavorites, favoritesInfo);
 
            var favoritesMembersByName = new Dictionary<string, MemberAndDeclarationInfo>();
 
            foreach (var member in allMembers)
            {
                // Favorites are currently never static
                if (member.IsFavorite && !value.IsNull)
                {
                    favoritesMembersByName.Add(member.DisplayName, member);
                }
                else if (member.IsStatic)
                {
                    staticMembers.Add(member);
                }
                else if (!value.IsNull)
                {
                    instanceMembers.Add(member);
                }
            }
 
            allMembers.Free();
 
            // Favorites members.
            Expansion favoritesExpansion = null;
            if (favoritesMembersByName.Count > 0)
            {
                var favoritesMembers = ArrayBuilder<MemberAndDeclarationInfo>.GetInstance();
 
                foreach (string name in favoritesInfo.Favorites)
                {
                    if (favoritesMembersByName.TryGetValue(name, out var memberAndDeclarationInfo))
                    {
                        favoritesMembers.Add(memberAndDeclarationInfo);
                    }
                }
 
                if (favoritesMembers.Count > 0)
                {
                    favoritesExpansion = new MemberExpansion(favoritesMembers.ToArrayAndFree(), customTypeInfoMap, containsFavorites: true);
                }
            }
 
            if (favoritesExpansion != null)
            {
                expansions.Add(favoritesExpansion);
 
                // Check if we are only expanding favorites.
                if ((inspectionContext.EvaluationFlags & DkmEvaluationFlags.FilterToFavorites) == DkmEvaluationFlags.FilterToFavorites)
                {
                    instanceMembers.Free();
                    staticMembers.Free();
                    expansions.Free();
 
                    return favoritesExpansion;
                }
            }
 
            // Public and non-public instance members.
            Expansion publicInstanceExpansion;
            Expansion nonPublicInstanceExpansion;
            GetPublicAndNonPublicMembers(
                instanceMembers,
                customTypeInfoMap,
                isProxyType,
                out publicInstanceExpansion,
                out nonPublicInstanceExpansion);
 
            // Public and non-public static members.
            Expansion publicStaticExpansion;
            Expansion nonPublicStaticExpansion;
            GetPublicAndNonPublicMembers(
                staticMembers,
                customTypeInfoMap,
                isProxyType,
                out publicStaticExpansion,
                out nonPublicStaticExpansion);
 
            if (publicInstanceExpansion != null)
            {
                expansions.Add(publicInstanceExpansion);
            }
 
            if ((publicStaticExpansion != null) || (nonPublicStaticExpansion != null))
            {
                var staticExpansions = ArrayBuilder<Expansion>.GetInstance();
                if (publicStaticExpansion != null)
                {
                    staticExpansions.Add(publicStaticExpansion);
                }
                if (nonPublicStaticExpansion != null)
                {
                    staticExpansions.Add(nonPublicStaticExpansion);
                }
                Debug.Assert(staticExpansions.Count > 0);
                var staticMembersExpansion = new StaticMembersExpansion(
                    type,
                    AggregateExpansion.CreateExpansion(staticExpansions));
                staticExpansions.Free();
                expansions.Add(staticMembersExpansion);
            }
 
            instanceMembers.Free();
            staticMembers.Free();
 
            if (value.NativeComPointer != 0)
            {
                expansions.Add(NativeViewExpansion.Instance);
            }
 
            if (nonPublicInstanceExpansion != null)
            {
                expansions.Add(nonPublicInstanceExpansion);
            }
 
            // Include Results View if necessary.
            if ((flags & ExpansionFlags.IncludeResultsView) != 0)
            {
                var resultsViewExpansion = ResultsViewExpansion.CreateExpansion(inspectionContext, value, resultProvider);
                if (resultsViewExpansion != null)
                {
                    expansions.Add(resultsViewExpansion);
                }
            }
 
            if (dynamicViewExpansion != null)
            {
                expansions.Add(dynamicViewExpansion);
            }
 
            var result = AggregateExpansion.CreateExpansion(expansions);
            expansions.Free();
            return result;
        }
 
        private static void GetPublicAndNonPublicMembers(
            ArrayBuilder<MemberAndDeclarationInfo> allMembers,
            CustomTypeInfoTypeArgumentMap customTypeInfoMap,
            bool isProxyType,
            out Expansion publicExpansion,
            out Expansion nonPublicExpansion)
        {
            var publicExpansions = ArrayBuilder<Expansion>.GetInstance();
            var publicMembers = ArrayBuilder<MemberAndDeclarationInfo>.GetInstance();
            var nonPublicMembers = ArrayBuilder<MemberAndDeclarationInfo>.GetInstance();
 
            foreach (var member in allMembers)
            {
                if (member.BrowsableState.HasValue)
                {
                    switch (member.BrowsableState.Value)
                    {
                        case DkmClrDebuggerBrowsableAttributeState.RootHidden:
                            if (publicMembers.Count > 0)
                            {
                                publicExpansions.Add(new MemberExpansion(publicMembers.ToArray(), customTypeInfoMap));
                                publicMembers.Clear();
                            }
                            publicExpansions.Add(new RootHiddenExpansion(member, customTypeInfoMap));
                            continue;
                        case DkmClrDebuggerBrowsableAttributeState.Never:
                            continue;
                    }
                }
 
                // The native EE shows proxy type members as public members if they have a
                // DebuggerBrowsable attribute of any value. Match that behaviour here.
                if (member.HideNonPublic && !member.IsPublic && (!isProxyType || !member.BrowsableState.HasValue))
                {
                    nonPublicMembers.Add(member);
                }
                else
                {
                    publicMembers.Add(member);
                }
            }
 
            if (publicMembers.Count > 0)
            {
                publicExpansions.Add(new MemberExpansion(publicMembers.ToArray(), customTypeInfoMap));
            }
            publicMembers.Free();
 
            publicExpansion = AggregateExpansion.CreateExpansion(publicExpansions);
            publicExpansions.Free();
 
            nonPublicExpansion = (nonPublicMembers.Count > 0)
                ? new NonPublicMembersExpansion(
                    members: new MemberExpansion(nonPublicMembers.ToArray(), customTypeInfoMap))
                : null;
            nonPublicMembers.Free();
        }
 
        private readonly MemberAndDeclarationInfo[] _members;
        private readonly CustomTypeInfoTypeArgumentMap _customTypeInfoMap;
        private readonly bool _containsFavorites;
 
        private MemberExpansion(MemberAndDeclarationInfo[] members, CustomTypeInfoTypeArgumentMap customTypeInfoMap, bool containsFavorites = false)
        {
            Debug.Assert(members != null);
            Debug.Assert(members.Length > 0);
            Debug.Assert(customTypeInfoMap != null);
 
            _members = members;
            _customTypeInfoMap = customTypeInfoMap;
            _containsFavorites = containsFavorites;
        }
 
        internal override bool ContainsFavorites => _containsFavorites;
 
        internal override void GetRows(
            ResultProvider resultProvider,
            ArrayBuilder<EvalResult> rows,
            DkmInspectionContext inspectionContext,
            EvalResultDataItem parent,
            DkmClrValue value,
            int startIndex,
            int count,
            bool visitAll,
            ref int index)
        {
            int startIndex2;
            int count2;
            GetIntersection(startIndex, count, index, _members.Length, out startIndex2, out count2);
 
            int offset = startIndex2 - index;
            for (int i = 0; i < count2; i++)
            {
                rows.Add(GetMemberRow(resultProvider, inspectionContext, value, _members[i + offset], parent, _customTypeInfoMap));
            }
 
            index += _members.Length;
        }
 
        private static EvalResult GetMemberRow(
            ResultProvider resultProvider,
            DkmInspectionContext inspectionContext,
            DkmClrValue value,
            MemberAndDeclarationInfo member,
            EvalResultDataItem parent,
            CustomTypeInfoTypeArgumentMap customTypeInfoMap)
        {
            var memberValue = value.GetMemberValue(member, inspectionContext);
            return CreateMemberDataItem(
                resultProvider,
                inspectionContext,
                member,
                memberValue,
                parent,
                customTypeInfoMap,
                ExpansionFlags.All,
                supportsFavorites: true);
        }
 
        /// <summary>
        /// An explicit user request to bypass "Just My Code" and display
        /// the inaccessible members of an instance of an imported type.
        /// </summary>
        private sealed class NonPublicMembersExpansion : Expansion
        {
            private readonly Expansion _members;
 
            internal NonPublicMembersExpansion(Expansion members)
            {
                _members = members;
            }
 
            internal override void GetRows(
                ResultProvider resultProvider,
                ArrayBuilder<EvalResult> rows,
                DkmInspectionContext inspectionContext,
                EvalResultDataItem parent,
                DkmClrValue value,
                int startIndex,
                int count,
                bool visitAll,
                ref int index)
            {
                if (InRange(startIndex, count, index))
                {
                    rows.Add(GetRow(
                        inspectionContext,
                        value,
                        _members,
                        parent));
                }
 
                index++;
            }
 
            private static readonly ReadOnlyCollection<string> s_hiddenFormatSpecifiers = new ReadOnlyCollection<string>(new[] { "hidden" });
 
            private static EvalResult GetRow(
                DkmInspectionContext inspectionContext,
                DkmClrValue value,
                Expansion expansion,
                EvalResultDataItem parent)
            {
                return new EvalResult(
                    ExpansionKind.NonPublicMembers,
                    name: Resources.NonPublicMembers,
                    typeDeclaringMemberAndInfo: default(TypeAndCustomInfo),
                    declaredTypeAndInfo: default(TypeAndCustomInfo),
                    useDebuggerDisplay: false,
                    value: value,
                    displayValue: null,
                    expansion: expansion,
                    childShouldParenthesize: parent.ChildShouldParenthesize,
                    fullName: parent.FullNameWithoutFormatSpecifiers,
                    childFullNamePrefixOpt: parent.ChildFullNamePrefix,
                    formatSpecifiers: s_hiddenFormatSpecifiers,
                    category: DkmEvaluationResultCategory.Data,
                    flags: DkmEvaluationResultFlags.ReadOnly,
                    editableValue: null,
                    inspectionContext: inspectionContext);
            }
        }
 
        /// <summary>
        /// A transition from an instance of a type to the type itself (for inspecting static members).
        /// </summary>
        private sealed class StaticMembersExpansion : Expansion
        {
            private readonly DkmClrType _type;
            private readonly Expansion _members;
 
            internal StaticMembersExpansion(DkmClrType type, Expansion members)
            {
                _type = type;
                _members = members;
            }
 
            internal override void GetRows(
                ResultProvider resultProvider,
                ArrayBuilder<EvalResult> rows,
                DkmInspectionContext inspectionContext,
                EvalResultDataItem parent,
                DkmClrValue value,
                int startIndex,
                int count,
                bool visitAll,
                ref int index)
            {
                if (InRange(startIndex, count, index))
                {
                    rows.Add(GetRow(
                        resultProvider,
                        inspectionContext,
                        new TypeAndCustomInfo(_type),
                        value,
                        _members));
                }
 
                index++;
            }
 
            private static EvalResult GetRow(
                ResultProvider resultProvider,
                DkmInspectionContext inspectionContext,
                TypeAndCustomInfo declaredTypeAndInfo,
                DkmClrValue value,
                Expansion expansion)
            {
                var fullName = resultProvider.FullNameProvider.GetClrTypeName(inspectionContext, declaredTypeAndInfo.ClrType, declaredTypeAndInfo.Info);
                return new EvalResult(
                    ExpansionKind.StaticMembers,
                    name: resultProvider.StaticMembersString,
                    typeDeclaringMemberAndInfo: default(TypeAndCustomInfo),
                    declaredTypeAndInfo: declaredTypeAndInfo,
                    useDebuggerDisplay: false,
                    value: value,
                    displayValue: null,
                    expansion: expansion,
                    childShouldParenthesize: false,
                    fullName: fullName,
                    childFullNamePrefixOpt: fullName,
                    formatSpecifiers: Formatter.NoFormatSpecifiers,
                    category: DkmEvaluationResultCategory.Class,
                    flags: DkmEvaluationResultFlags.ReadOnly,
                    editableValue: null,
                    inspectionContext: inspectionContext);
            }
        }
#nullable enable
        internal static EvalResult CreateMemberDataItem(
            ResultProvider resultProvider,
            DkmInspectionContext inspectionContext,
            MemberAndDeclarationInfo member,
            DkmClrValue memberValue,
            EvalResultDataItem parent,
            CustomTypeInfoTypeArgumentMap customTypeInfoMap,
            ExpansionFlags flags,
            bool supportsFavorites)
        {
            var fullNameProvider = resultProvider.FullNameProvider;
            var declaredType = member.Type;
            var declaredTypeInfo = customTypeInfoMap.SubstituteCustomTypeInfo(member.OriginalDefinitionType, member.TypeInfo);
            string? memberNameForFullName;
 
            // Considering, we're not handling the case of a member inherited from a generic base type.
            if (member.TryGetExplicitlyImplementedInterface(out var declaringType, out var memberDisplayName))
            {
                // full name will include cast to the interface, e.g. "((I)obj).MemberName":
                memberNameForFullName = memberDisplayName;
            }
            else
            {
                declaringType = member.DeclaringType;
                memberDisplayName = member.DisplayName;
                memberNameForFullName = member.MetadataName;
            }
 
            var declaringTypeInfo = declaringType.IsInterface
                ? customTypeInfoMap.SubstituteCustomTypeInfo(declaringType.GetInterfaceListEntry(member.DeclaringType), customInfo: null)
                : null;
 
            var appDomain = memberValue.Type.AppDomain;
            string? fullName;
 
            memberNameForFullName = fullNameProvider.GetClrValidIdentifier(inspectionContext, memberNameForFullName);
            if (memberNameForFullName == null)
            {
                fullName = null;
            }
            else
            {
                memberDisplayName = memberNameForFullName;
                fullName = MakeFullName(
                       fullNameProvider,
                       inspectionContext,
                       memberNameForFullName,
                       new TypeAndCustomInfo(DkmClrType.Create(appDomain, declaringType), declaringTypeInfo), // Note: Won't include DynamicAttribute.
                       member.RequiresExplicitCast,
                       member.IsStatic,
                       parent);
            }
 
            return resultProvider.CreateDataItem(
                inspectionContext,
                memberDisplayName,
                typeDeclaringMemberAndInfo: (member.IncludeTypeInMemberName || declaringType.IsInterface) ? new TypeAndCustomInfo(DkmClrType.Create(appDomain, declaringType), declaringTypeInfo) : default(TypeAndCustomInfo), // Note: Won't include DynamicAttribute.
                declaredTypeAndInfo: new TypeAndCustomInfo(DkmClrType.Create(appDomain, declaredType), declaredTypeInfo),
                value: memberValue,
                useDebuggerDisplay: parent != null,
                expansionFlags: flags,
                childShouldParenthesize: false,
                fullName: fullName,
                formatSpecifiers: Formatter.NoFormatSpecifiers,
                category: DkmEvaluationResultCategory.Other,
                flags: memberValue.EvalFlags,
                evalFlags: DkmEvaluationFlags.None,
                canFavorite: member.CanFavorite,
                isFavorite: member.IsFavorite,
                supportsFavorites: supportsFavorites);
        }
#nullable disable
        private static string MakeFullName(
            IDkmClrFullNameProvider fullNameProvider,
            DkmInspectionContext inspectionContext,
            string name,
            TypeAndCustomInfo typeDeclaringMemberAndInfo,
            bool memberAccessRequiresExplicitCast,
            bool memberIsStatic,
            EvalResultDataItem parent)
        {
            // If the parent is an exception thrown during evaluation,
            // there is no valid fullname expression for the child.
            if (parent.Value.EvalFlags.Includes(DkmEvaluationResultFlags.ExceptionThrown))
            {
                return null;
            }
 
            var parentFullName = parent.ChildFullNamePrefix;
            if (parentFullName == null)
            {
                return null;
            }
 
            if (parent.ChildShouldParenthesize)
            {
                parentFullName = parentFullName.Parenthesize();
            }
 
            var typeDeclaringMember = typeDeclaringMemberAndInfo.Type;
            if (typeDeclaringMember.IsInterface)
            {
                memberAccessRequiresExplicitCast = !typeDeclaringMember.Equals(parent.DeclaredTypeAndInfo.Type);
            }
 
            return fullNameProvider.GetClrMemberName(
                inspectionContext,
                parentFullName,
                typeDeclaringMemberAndInfo.ClrType,
                typeDeclaringMemberAndInfo.Info,
                name,
                memberAccessRequiresExplicitCast,
                memberIsStatic);
        }
    }
}